Skip to content

Commit

Permalink
feat: re-implement service API with proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Oct 21, 2023
1 parent 13b6cdd commit 5852859
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 131 deletions.
237 changes: 134 additions & 103 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,44 @@ import { Lifecycle } from './events'
import { Registry } from './registry'
import { getConstructor, isConstructor, resolveConfig } from './utils'

export namespace Context {
export type Parameterized<C, T = any> = Omit<C, 'config'> & { config: T }

export interface Config extends Lifecycle.Config, Registry.Config {}

export interface MixinOptions {
methods?: string[]
accessors?: string[]
prototype?: any
}

export type Internal = Internal.Service | Internal.Accessor | Internal.Method

export namespace Internal {
export interface Service {
type: 'service'
key: symbol
prototype?: {}
}

export interface Accessor {
type: 'accessor'
service: keyof any
}

export interface Method {
type: 'method'
service: keyof any
}
}
}

export interface Context<T = any> {
[Context.config]: Context.Config
root: Context.Configured<this, this[typeof Context.config]>
[Context.internal]: Record<keyof any, Context.Internal>
root: Context.Parameterized<this, this[typeof Context.config]>
mapping: Record<string | symbol, symbol>
realms: Record<string, Record<string, symbol>>
lifecycle: Lifecycle
registry: Registry<this>
config: T
Expand All @@ -22,140 +56,137 @@ export class Context {
static readonly current = Symbol('current')
static readonly internal = Symbol('internal')

constructor(config?: Context.Config) {
const options = resolveConfig(getConstructor(this), config)
const attach = (internal: {}) => {
if (!internal) return
attach(Object.getPrototypeOf(internal))
for (const key of Object.getOwnPropertySymbols(internal)) {
const constructor = internal[key]
const name = constructor[Context.expose]
this[key] = new constructor(this, name ? options?.[name] : options)
}
private static ensureInternal(): Context[typeof Context.internal] {
if (Object.prototype.hasOwnProperty.call(this.prototype, this.internal)) {
return this.prototype[this.internal]
}

this.root = this as any
this.mapping = Object.create(null)
attach(this[Context.internal])
}

[Symbol.for('nodejs.util.inspect.custom')]() {
return `Context <${this.runtime.name}>`
}

get events() {
return this.lifecycle
const parent = Object.getPrototypeOf(this).ensureInternal()
return this.prototype[this.internal] = Object.create(parent)
}

/** @deprecated */
get state() {
return this.scope
}

extend(meta = {}): this {
return Object.assign(Object.create(this), meta)
}

isolate(names: string[]) {
const mapping = Object.create(this.mapping)
for (const name of names) {
mapping[name] = Symbol(name)
}
return this.extend({ mapping })
}
}

export namespace Context {
export type Configured<C, T = any> = Omit<C, 'config'> & { config: T }

export interface Config extends Lifecycle.Config, Registry.Config {}

export interface MixinOptions {
methods?: string[]
properties?: string[]
}

export function mixin(name: keyof any, options: MixinOptions) {
static mixin(name: keyof any, options: Context.MixinOptions) {
const internal = this.ensureInternal()
for (const key of options.methods || []) {
const method = defineProperty(function (this: Context, ...args: any[]) {
return this[name][key](...args)
}, 'name', key)
defineProperty(this.prototype, key, method)
internal[key] = { type: 'method', service: name }
}

for (const key of options.properties || []) {
Object.defineProperty(this.prototype, key, {
configurable: true,
get(this: Context) {
return this[name][key]
},
set(this: Context, value: any) {
this[name][key] = value
},
})
for (const key of options.accessors || []) {
internal[key] = { type: 'accessor', service: name }
}
}

export interface ServiceOptions extends MixinOptions {
prototype?: any
/** @deprecated */
static service(name: keyof any, options: Context.MixinOptions = {}) {
const internal = this.ensureInternal()
const key = typeof name === 'symbol' ? name : Symbol(name)
internal[name] = { type: 'service', key }
if (isConstructor(options)) {
internal[name]['prototype'] = options.prototype
}
this.mixin(name, options)
}

export function service(name: keyof any, options: ServiceOptions = {}) {
if (Object.prototype.hasOwnProperty.call(this.prototype, name)) return
const privateKey = typeof name === 'symbol' ? name : Symbol(name)

Object.defineProperty(this.prototype, name, {
configurable: true,
get(this: Context) {
const key = this.mapping[name as any] || privateKey
const value = this.root[key]
static handler: ProxyHandler<Context> = {
get(target, name, receiver) {
if (typeof name !== 'string') return Reflect.get(target, name, receiver)
const internal = receiver[Context.internal][name]
if (!internal) return Reflect.get(target, name, receiver)
if (internal.type === 'accessor') {
return Reflect.get(receiver[internal.service], name)
} else if (internal.type === 'method') {
return defineProperty(function (this: Context, ...args: any[]) {
return this[internal.service][name](...args)
}, 'name', name)
} else if (internal.type === 'service') {
const privateKey = receiver.mapping[name] || internal.key
const value = receiver.root[privateKey]
if (!value) return
defineProperty(value, Context.current, this)
defineProperty(value, Context.current, receiver)
return value
},
set(this: Context, value) {
const key = this.mapping[name] || privateKey
const oldValue = this.root[key]
if (oldValue === value) return
}
},
set(target, name, value, receiver) {
if (typeof name !== 'string') return Reflect.set(target, name, value, receiver)
const internal = receiver[Context.internal][name]
if (!internal) return Reflect.set(target, name, value, receiver)
if (internal.type === 'accessor') {
return Reflect.set(receiver[internal.service], name, value)
} else if (internal.type === 'service') {
const key = receiver.mapping[name] || internal.key
const oldValue = receiver.root[key]
if (oldValue === value) return true

// setup filter for events
const self = Object.create(null)
self[Context.filter] = (ctx: Context) => {
return this.mapping[name] === ctx.mapping[name]
return receiver.mapping[name] === ctx.mapping[name]
}

// check override
if (value && oldValue && typeof name === 'string') {
throw new Error(`service ${name} has been registered`)
}

if (typeof name === 'string') {
this.emit(self, 'internal/before-service', name, value)
if (typeof name === 'string' && !internal.prototype) {
receiver.root.emit(self, 'internal/before-service', name, value)
}
this.root[key] = value
receiver.root[key] = value
if (value && typeof value === 'object') {
defineProperty(value, Context.source, this)
defineProperty(value, Context.source, receiver)
}
if (typeof name === 'string') {
this.emit(self, 'internal/service', name, oldValue)
if (typeof name === 'string' && !internal.prototype) {
receiver.root.emit(self, 'internal/service', name, oldValue)
}
},
})
return true
}
return false
},
}

if (isConstructor(options)) {
const internal = ensureInternal(this.prototype)
internal[privateKey] = options
constructor(config?: Context.Config) {
const self = new Proxy(Object.create(Object.getPrototypeOf(this)), Context.handler)
config = resolveConfig(getConstructor(this), config)
self.root = self as any
self.mapping = Object.create(null)
self.realms = Object.create(null)

const attach = (internal: Context[typeof Context.internal]) => {
if (!internal) return
attach(Object.getPrototypeOf(internal))
for (const key of [...Object.getOwnPropertyNames(internal), ...Object.getOwnPropertySymbols(internal)]) {
const constructor = internal[key]['prototype']?.constructor
if (!constructor) continue
const name = constructor[Context.expose]
self[key] = new constructor(self, name ? config?.[name] : config)
}
}
attach(this[Context.internal])
return self
}

this.mixin(name, options)
[Symbol.for('nodejs.util.inspect.custom')]() {
return `Context <${this.runtime.name}>`
}

get events() {
return this.lifecycle
}

function ensureInternal(prototype: {}) {
if (Object.prototype.hasOwnProperty.call(prototype, Context.internal)) {
return prototype[Context.internal]
/** @deprecated */
get state() {
return this.scope
}

extend(meta = {}): this {
return Object.assign(Object.create(this), meta)
}

isolate(names: string[], label?: string) {
const mapping = Object.create(this.mapping)
for (const name of names) {
mapping[name] = label ? ((this.realms[label] ??= Object.create(null))[name] ??= Symbol(name)) : Symbol(name)
}
const parent = ensureInternal(Object.getPrototypeOf(prototype))
return prototype[Context.internal] = Object.create(parent)
return this.extend({ mapping })
}
}

Expand All @@ -164,7 +195,7 @@ Context.prototype[Context.internal] = Object.create(null)
Context.service('registry', Registry)
Context.service('lifecycle', Lifecycle)

Context.mixin('state', {
properties: ['config', 'runtime'],
Context.mixin('scope', {
accessors: ['config', 'runtime'],
methods: ['collect', 'accept', 'decline'],
})
14 changes: 8 additions & 6 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type GetEvents<C extends Context> = C[typeof Context.events]

declare module './context' {
export interface Context {
/* eslint-disable max-len */
[Context.events]: Events<this>
parallel<K extends keyof GetEvents<this>>(name: K, ...args: Parameters<GetEvents<this>[K]>): Promise<void>
parallel<K extends keyof GetEvents<this>>(thisArg: ThisType<GetEvents<this>[K]>, name: K, ...args: Parameters<GetEvents<this>[K]>): Promise<void>
Expand All @@ -28,6 +29,7 @@ declare module './context' {
off<K extends keyof GetEvents<this>>(name: K, listener: GetEvents<this>[K]): boolean
start(): Promise<void>
stop(): Promise<void>
/* eslint-enable max-len */
}
}

Expand All @@ -38,7 +40,7 @@ export namespace Lifecycle {
}

export class Lifecycle {
static readonly methods = ['on', 'once', 'off', 'before', 'after', 'parallel', 'emit', 'serial', 'bail', 'start', 'stop']
static readonly methods = ['on', 'once', 'off', 'after', 'parallel', 'emit', 'serial', 'bail', 'start', 'stop']

isActive = false
_tasks = new Set<Promise<void>>()
Expand Down Expand Up @@ -180,13 +182,13 @@ export interface Events<C extends Context = Context> {
'fork': Plugin.Function<C['config'], C>
'ready'(): Awaitable<void>
'dispose'(): Awaitable<void>
'internal/fork'(fork: ForkScope<Context.Configured<C>>): void
'internal/runtime'(runtime: MainScope<Context.Configured<C>>): void
'internal/status'(scope: EffectScope<Context.Configured<C>>, oldValue: ScopeStatus): void
'internal/fork'(fork: ForkScope<Context.Parameterized<C>>): void
'internal/runtime'(runtime: MainScope<Context.Parameterized<C>>): void
'internal/status'(scope: EffectScope<Context.Parameterized<C>>, oldValue: ScopeStatus): void
'internal/warning'(format: any, ...param: any[]): void
'internal/before-service'(name: string, value: any): void
'internal/service'(name: string, oldValue: any): void
'internal/before-update'(fork: ForkScope<Context.Configured<C>>, config: any): void
'internal/update'(fork: ForkScope<Context.Configured<C>>, oldConfig: any): void
'internal/before-update'(fork: ForkScope<Context.Parameterized<C>>, config: any): void
'internal/update'(fork: ForkScope<Context.Parameterized<C>>, oldConfig: any): void
'internal/hook'(this: Lifecycle, name: string, listener: Function, prepend: boolean): () => boolean
}
6 changes: 3 additions & 3 deletions src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export namespace Plugin {

declare module './context' {
export interface Context {
using(deps: readonly string[], callback: Plugin.Function<void, Context.Configured<this>>): ForkScope<Context.Configured<this>>
plugin<S extends Plugin<Context.Configured<this>>, T extends Plugin.Config<S>>(plugin: S, config?: boolean | T): ForkScope<Context.Configured<this, T>>
using(deps: readonly string[], callback: Plugin.Function<void, Context.Parameterized<this>>): ForkScope<Context.Parameterized<this>>
plugin<S extends Plugin<Context.Parameterized<this>>, T extends Plugin.Config<S>>(plugin: S, config?: T): ForkScope<Context.Parameterized<this, T>>
/** @deprecated use `ctx.registry.delete()` instead */
dispose(plugin?: Plugin<Context.Configured<this>>): boolean
dispose(plugin?: Plugin<Context.Parameterized<this>>): boolean
}
}

Expand Down
Loading

0 comments on commit 5852859

Please sign in to comment.