Skip to content

Commit

Permalink
feat: extended builtin service
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Oct 24, 2023
1 parent 2fd92c7 commit b21ada0
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 46 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cordis",
"description": "AOP Framework for Modern JavaScript Applications",
"version": "2.10.2",
"version": "3.0.0",
"sideEffects": false,
"main": "lib/index.cjs",
"module": "lib/index.mjs",
Expand Down
42 changes: 22 additions & 20 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export namespace Context {
export interface Service {
type: 'service'
key: symbol
builtin?: boolean
prototype?: {}
}

Expand All @@ -43,15 +44,15 @@ export interface Context<T = any> {
}

export class Context {
static readonly config = Symbol('config')
static readonly events = Symbol('events')
static readonly static = Symbol('static')
static readonly filter = Symbol('filter')
static readonly source = Symbol('source')
static readonly expose = Symbol('expose')
static readonly shadow = Symbol('shadow')
static readonly current = Symbol('current')
static readonly internal = Symbol('internal')
static readonly config = Symbol.for('cordis.config')
static readonly events = Symbol.for('cordis.events')
static readonly static = Symbol.for('cordis.static')
static readonly filter = Symbol.for('cordis.filter')
static readonly source = Symbol.for('cordis.source')
static readonly expose = Symbol.for('cordis.expose')
static readonly shadow = Symbol.for('cordis.shadow')
static readonly current = Symbol.for('cordis.current')
static readonly internal = Symbol.for('cordis.internal')

private static ensureInternal(): Context[typeof Context.internal] {
const ctx = this.prototype || this
Expand Down Expand Up @@ -90,14 +91,14 @@ export class Context {
if (typeof name !== 'string') return Reflect.get(target, name, ctx)

const checkInject = (name: string) => {
// Case 1: a normal property defined on `target`
// Case 1: a normal property defined on context
if (Reflect.has(target, name)) return
// Case 2: built-in services and special properties
// - prototype: prototype detection
// - then: async function return
if (['prototype', 'then', 'registry', 'lifecycle'].includes(name)) return
// Case 3: declared as plugin injection
if (ctx.runtime.inject.has(name)) return
// Case 3: access from root or declared as `inject`
if (!ctx.runtime.plugin || ctx.runtime.inject.has(name)) return
ctx.emit('internal/warning', new Error(`property ${name} is not registered, declare it as \`inject\` to suppress this warning`))
}

Expand All @@ -114,13 +115,14 @@ export class Context {
if (typeof value !== 'function') return value
return value.bind(service)
} else if (internal.type === 'service') {
checkInject(name)
return ctx.get(name)
if (!internal.builtin) checkInject(name)
return ctx.get(name as any)
}
},

set(target, name, value, ctx: Context) {
if (typeof name !== 'string') return Reflect.set(target, name, value, ctx)

const internal = ctx[Context.internal][name]
if (!internal) return Reflect.set(target, name, value, ctx)
if (internal.type === 'mixin') {
Expand Down Expand Up @@ -165,8 +167,8 @@ export class Context {
self.mixin('scope', ['config', 'runtime', 'collect', 'accept', 'decline'])
self.mixin('registry', ['using', 'plugin', 'dispose'])
self.mixin('lifecycle', ['on', 'once', 'off', 'after', 'parallel', 'emit', 'serial', 'bail', 'start', 'stop'])
self.provide('registry', new Registry(self, config!))
self.provide('lifecycle', new Lifecycle(self))
self.provide('registry', new Registry(self, config!), true)
self.provide('lifecycle', new Lifecycle(self), true)

const attach = (internal: Context[typeof Context.internal]) => {
if (!internal) return
Expand Down Expand Up @@ -203,10 +205,10 @@ export class Context {
return this.scope
}

get(name: string) {
get<K extends string & keyof this>(name: K): undefined | this[K] {
const internal = this[Context.internal][name]
if (internal?.type !== 'service') return
const key = this[Context.shadow][name] || internal.key
const key: symbol = this[Context.shadow][name] || internal.key
const value = this.root[key]
if (!value || typeof value !== 'object') return value
if (isUnproxyable(value)) {
Expand All @@ -221,10 +223,10 @@ export class Context {
})
}

provide(name: string, value?: any) {
provide(name: string, value?: any, builtin?: boolean) {
const internal = Context.ensureInternal.call(this)
const key = Symbol(name)
internal[name] = { type: 'service', key }
internal[name] = { type: 'service', key, builtin }
this[key] = value
}

Expand Down
1 change: 0 additions & 1 deletion tests/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ describe('Plugin', () => {
})

await root.lifecycle.flush()
expect(root.registry.size).to.equal(5)
root.registry.forEach((scope) => {
if (scope.error) throw scope.error
})
Expand Down
62 changes: 38 additions & 24 deletions tests/service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,57 @@ import * as jest from 'jest-mock'
import { getHookSnapshot } from './utils'

describe('Service', () => {
it('service access', () => {
it('service access', async () => {
const root = new Context()
const warn = jest.fn()
root.on('internal/warning', warn)
root.provide('foo')

// service should be proxyable
root.foo = new Set()
expect(warn.mock.calls).to.have.length(1)

// `foo` is not declared as injection
root.foo.add(1)
expect(warn.mock.calls).to.have.length(2)
root.plugin((ctx) => {
// service should be proxyable
ctx.foo = new Set()
expect(warn.mock.calls).to.have.length(1)

// `foo` is not declared as injection
ctx.foo.add(1)
expect(warn.mock.calls).to.have.length(2)

// service cannot be overwritten
expect(() => ctx.foo = new Set()).to.throw()
})

// service cannot be overwritten
expect(() => root.foo = new Set()).to.throw()
await root.lifecycle.flush()
root.registry.forEach((scope) => {
if (scope.error) throw scope.error
})
})

it('non-service access', () => {
it('non-service access', async () => {
const root = new Context()
const warn = jest.fn()
root.on('internal/warning', warn)

// `bar` is neither defined on context nor declared as injection
root.bar
expect(warn.mock.calls).to.have.length(1)

// non-service can be unproxyable
root.bar = new Set()
expect(warn.mock.calls).to.have.length(1)

// non-service can be accessed if defined on context
root.bar.add(1)
expect(warn.mock.calls).to.have.length(1)
root.plugin((ctx) => {
// `bar` is neither defined on context nor declared as injection
ctx.bar
expect(warn.mock.calls).to.have.length(1)

// non-service can be unproxyable
ctx.bar = new Set()
expect(warn.mock.calls).to.have.length(1)

// non-service can be accessed if defined on context
ctx.bar.add(1)
expect(warn.mock.calls).to.have.length(1)

// non-service can be overwritten
expect(() => ctx.bar = new Set()).to.not.throw()
})

// non-service can be overwritten
expect(() => root.bar = new Set()).to.not.throw()
await root.lifecycle.flush()
root.registry.forEach((scope) => {
if (scope.error) throw scope.error
})
})

it('normal service', async () => {
Expand Down

0 comments on commit b21ada0

Please sign in to comment.