mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
feat(client): add ownership lifecycle primitives
This commit is contained in:
1 parent
98ebcccf9e
commit
c52ef9fada
7 files changed
+197
-4
No files matched your search
@@ -9,6 +9,8 @@ import { Service, type Context } from '@deepseek-ai/cordis'
|
||||
import type { TypertContextMap } from './types.ts'
|
||||
|
||||
export { RemoteError, remoteErrorOf } from './remote-error.ts'
|
||||
export { TYPERT_OWNED_VALUE, isTypertOwnedValue, typertOwnedValue } from './owned-value.ts'
|
||||
export type { TypertOwnedValue } from './owned-value.ts'
|
||||
|
||||
const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/** Generic invocation-owned values returned by synchronous Client Context resolvers. */
|
||||
|
||||
/** Shared identity across independently bundled Context providers and Gateway. */
|
||||
export const TYPERT_OWNED_VALUE = Symbol.for('dsh.typert.owned-value')
|
||||
|
||||
/** A borrowed payload paired with the invocation owner's idempotent cleanup. */
|
||||
export interface TypertOwnedValue<Value> extends Disposable {
|
||||
readonly [TYPERT_OWNED_VALUE]: true
|
||||
readonly value: Value
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer cleanup ownership without adding another resource reference count.
|
||||
* @param value - resolved payload passed to the invocation.
|
||||
* @param release - non-throwing synchronous release, called at most once.
|
||||
* @returns an owned payload disposed after invocation and reply settlement.
|
||||
*/
|
||||
export function typertOwnedValue<Value>(value: Value, release: () => void): TypertOwnedValue<Value> {
|
||||
let active = true
|
||||
return {
|
||||
[TYPERT_OWNED_VALUE]: true,
|
||||
value,
|
||||
[Symbol.dispose]() {
|
||||
if (!active) return
|
||||
active = false
|
||||
release()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify invocation-owned values using the shared marker.
|
||||
* @param value - borrowed or owned resolver result.
|
||||
* @returns whether the result carries invocation cleanup.
|
||||
*/
|
||||
export function isTypertOwnedValue(value: unknown): value is TypertOwnedValue<unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
&& TYPERT_OWNED_VALUE in value && value[TYPERT_OWNED_VALUE] === true
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { Context, Events } from '@deepseek-ai/cordis'
|
||||
import type { TypertOwnedValue } from './owned-value.ts'
|
||||
|
||||
declare const LOOKUP_HOST: unique symbol
|
||||
declare const LOOKUP_WIRE: unique symbol
|
||||
@@ -393,11 +394,11 @@ export interface TypertClientContextAdapter<Wire = unknown> {
|
||||
*/
|
||||
identity(ctx: Context): Wire | undefined
|
||||
/**
|
||||
* Resolve a wire identity from the Client's currently materialized Contexts.
|
||||
* Resolve a validated identity synchronously for one Client invocation.
|
||||
* @param id - validated wire identity.
|
||||
* @returns the Client Context, or `undefined` when unavailable.
|
||||
* @returns a borrowed or invocation-owned Client Context, or undefined when unavailable.
|
||||
*/
|
||||
resolve(id: Wire): Context | undefined
|
||||
resolve(id: Wire): Context | TypertOwnedValue<Context> | undefined
|
||||
}
|
||||
|
||||
/** Notification emitted after a Typert runtime registry changes. */
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/** Invocation ownership remains generic, idempotent, and shared across bundles. */
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { isTypertOwnedValue, TYPERT_OWNED_VALUE, typertOwnedValue } from '../src/owned-value.ts'
|
||||
|
||||
describe('TypertOwnedValue', () => {
|
||||
it('preserves its value and releases once across explicit and scoped disposal', () => {
|
||||
const value = { context: 'owned' }
|
||||
const release = vi.fn()
|
||||
{
|
||||
using owned = typertOwnedValue(value, release)
|
||||
expect(owned.value).toBe(value)
|
||||
expect(isTypertOwnedValue(owned)).toBe(true)
|
||||
owned[Symbol.dispose]()
|
||||
owned[Symbol.dispose]()
|
||||
}
|
||||
expect(release).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('recognizes a separately produced marker without accepting false markers or borrowed values', () => {
|
||||
expect(TYPERT_OWNED_VALUE).toBe(Symbol.for('dsh.typert.owned-value'))
|
||||
const siblingBundle = { [Symbol.for('dsh.typert.owned-value')]: true, value: {}, [Symbol.dispose]() {} }
|
||||
expect(isTypertOwnedValue(siblingBundle)).toBe(true)
|
||||
for (const value of [undefined, null, 1, {}, { [Symbol.for('dsh.typert.owned-value')]: false }]) {
|
||||
expect(isTypertOwnedValue(value)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -237,3 +237,70 @@ export function deepFreeze<T>(value: T): T {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Weak-key lookup with a strongly retained iterable set of associated values.
|
||||
*
|
||||
* Each value must belong to only one key. The container performs no automatic
|
||||
* cleanup; owners delete associations or clear the container at lifecycle end.
|
||||
*/
|
||||
export class WeakMapWithValues<Key extends object, Value> {
|
||||
private keys = new WeakMap<Key, Value>()
|
||||
private readonly valueSet = new Set<Value>()
|
||||
/** Live strongly retained values in insertion order. */
|
||||
readonly values: ReadonlySet<Value> = this.valueSet
|
||||
|
||||
/**
|
||||
* Read the value associated with a key.
|
||||
* @param key - weakly held lookup key.
|
||||
* @returns the associated value, or absence.
|
||||
*/
|
||||
get(key: Key): Value | undefined {
|
||||
return this.keys.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a key has an association.
|
||||
* @param key - weakly held lookup key.
|
||||
* @returns whether the key is present.
|
||||
*/
|
||||
has(key: Key): boolean {
|
||||
return this.keys.has(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate one key with one caller-unique value.
|
||||
* @param key - weakly held lookup key.
|
||||
* @param value - strongly retained value that belongs to no other key.
|
||||
* @returns this container.
|
||||
*/
|
||||
set(key: Key, value: Value): this {
|
||||
if (this.keys.has(key)) {
|
||||
const previous = this.keys.get(key) as Value
|
||||
if (previous === value) return this
|
||||
this.valueSet.delete(previous)
|
||||
}
|
||||
this.keys.set(key, value)
|
||||
this.valueSet.add(value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one association and its strongly retained value.
|
||||
* @param key - weakly held lookup key.
|
||||
* @returns whether an association was removed.
|
||||
*/
|
||||
delete(key: Key): boolean {
|
||||
if (!this.keys.has(key)) return false
|
||||
const value = this.keys.get(key) as Value
|
||||
const deleted = this.keys.delete(key)
|
||||
this.valueSet.delete(value)
|
||||
return deleted
|
||||
}
|
||||
|
||||
/** Remove every association and strongly retained value. */
|
||||
clear(): void {
|
||||
this.keys = new WeakMap()
|
||||
this.valueSet.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { WeakMapWithValues } from '../src/index.ts'
|
||||
|
||||
describe('WeakMapWithValues', () => {
|
||||
it('looks up weak keys while iterating strongly retained values in insertion order', () => {
|
||||
const firstKey = {}
|
||||
const secondKey = {}
|
||||
const firstValue = { name: 'first' }
|
||||
const secondValue = { name: 'second' }
|
||||
const values = new WeakMapWithValues<object, { name: string }>()
|
||||
|
||||
expect(values.get(firstKey)).toBeUndefined()
|
||||
expect(values.has(firstKey)).toBe(false)
|
||||
expect(values.set(firstKey, firstValue)).toBe(values)
|
||||
values.set(secondKey, secondValue)
|
||||
|
||||
expect(values.get(firstKey)).toBe(firstValue)
|
||||
expect(values.has(secondKey)).toBe(true)
|
||||
expect([...values.values]).toEqual([firstValue, secondValue])
|
||||
})
|
||||
|
||||
it('keeps an identical association and replaces its prior retained value', () => {
|
||||
const key = {}
|
||||
const firstValue = { name: 'first' }
|
||||
const replacement = { name: 'replacement' }
|
||||
const values = new WeakMapWithValues<object, { name: string }>()
|
||||
|
||||
values.set(key, firstValue)
|
||||
values.set(key, firstValue)
|
||||
expect([...values.values]).toEqual([firstValue])
|
||||
|
||||
values.set(key, replacement)
|
||||
expect(values.get(key)).toBe(replacement)
|
||||
expect([...values.values]).toEqual([replacement])
|
||||
})
|
||||
|
||||
it('deletes one association and clears every lookup and retained value', () => {
|
||||
const firstKey = {}
|
||||
const secondKey = {}
|
||||
const values = new WeakMapWithValues<object, string>()
|
||||
values.set(firstKey, 'first').set(secondKey, 'second')
|
||||
|
||||
expect(values.delete({})).toBe(false)
|
||||
expect(values.delete(firstKey)).toBe(true)
|
||||
expect(values.has(firstKey)).toBe(false)
|
||||
expect([...values.values]).toEqual(['second'])
|
||||
|
||||
values.clear()
|
||||
expect(values.get(secondKey)).toBeUndefined()
|
||||
expect(values.has(secondKey)).toBe(false)
|
||||
expect([...values.values]).toEqual([])
|
||||
|
||||
values.set(firstKey, 'again')
|
||||
expect(values.get(firstKey)).toBe('again')
|
||||
expect([...values.values]).toEqual(['again'])
|
||||
})
|
||||
})
|
||||
@@ -5,7 +5,7 @@
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2024", "DOM", "DOM.Iterable", "ESNext.Disposable"],
|
||||
"typeRoots": ["./scripts/types", "./node_modules/@types"],
|
||||
"types": ["client-build-environment"]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user