mvc-kit 2.11.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +41 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +43 -1
- package/agent-config/claude-code/skills/guide/patterns.md +64 -0
- package/agent-config/copilot/copilot-instructions.md +10 -6
- package/agent-config/cursor/cursorrules +10 -6
- package/dist/Feed.cjs +10 -22
- package/dist/Feed.cjs.map +1 -1
- package/dist/Feed.d.ts +2 -5
- package/dist/Feed.d.ts.map +1 -1
- package/dist/Feed.js +10 -22
- package/dist/Feed.js.map +1 -1
- package/dist/Pagination.cjs +8 -20
- package/dist/Pagination.cjs.map +1 -1
- package/dist/Pagination.d.ts +2 -5
- package/dist/Pagination.d.ts.map +1 -1
- package/dist/Pagination.js +8 -20
- package/dist/Pagination.js.map +1 -1
- package/dist/Pending.cjs +26 -39
- package/dist/Pending.cjs.map +1 -1
- package/dist/Pending.d.ts +5 -9
- package/dist/Pending.d.ts.map +1 -1
- package/dist/Pending.js +26 -39
- package/dist/Pending.js.map +1 -1
- package/dist/Selection.cjs +5 -13
- package/dist/Selection.cjs.map +1 -1
- package/dist/Selection.d.ts +2 -4
- package/dist/Selection.d.ts.map +1 -1
- package/dist/Selection.js +5 -13
- package/dist/Selection.js.map +1 -1
- package/dist/Sorting.cjs +7 -19
- package/dist/Sorting.cjs.map +1 -1
- package/dist/Sorting.d.ts +2 -5
- package/dist/Sorting.d.ts.map +1 -1
- package/dist/Sorting.js +7 -19
- package/dist/Sorting.js.map +1 -1
- package/dist/Trackable.cjs +81 -0
- package/dist/Trackable.cjs.map +1 -0
- package/dist/Trackable.d.ts +82 -0
- package/dist/Trackable.d.ts.map +1 -0
- package/dist/Trackable.js +81 -0
- package/dist/Trackable.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +4 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +4 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/guards.cjs +2 -0
- package/dist/react/guards.cjs.map +1 -1
- package/dist/react/guards.d.ts +4 -0
- package/dist/react/guards.d.ts.map +1 -1
- package/dist/react/guards.js +3 -1
- package/dist/react/guards.js.map +1 -1
- package/dist/react/use-local.cjs +5 -0
- package/dist/react/use-local.cjs.map +1 -1
- package/dist/react/use-local.d.ts.map +1 -1
- package/dist/react/use-local.js +6 -1
- package/dist/react/use-local.js.map +1 -1
- package/dist/react/use-singleton.cjs +5 -0
- package/dist/react/use-singleton.cjs.map +1 -1
- package/dist/react/use-singleton.d.ts.map +1 -1
- package/dist/react/use-singleton.js +6 -1
- package/dist/react/use-singleton.js.map +1 -1
- package/dist/react/use-subscribe-only.cjs +25 -0
- package/dist/react/use-subscribe-only.cjs.map +1 -0
- package/dist/react/use-subscribe-only.d.ts +9 -0
- package/dist/react/use-subscribe-only.d.ts.map +1 -0
- package/dist/react/use-subscribe-only.js +25 -0
- package/dist/react/use-subscribe-only.js.map +1 -0
- package/package.json +2 -1
package/dist/Pending.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pending.js","sources":["../src/Pending.ts"],"sourcesContent":["import { classifyError, isAbortError } from './errors';\nimport type { AppError } from './errors';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Types ─────────────────────────────────────────────────────────\n\n/** Frozen snapshot of a single pending operation's state. */\nexport interface PendingOperation<Meta = unknown> {\n readonly status: 'active' | 'retrying' | 'failed';\n readonly operation: string;\n readonly attempts: number;\n readonly maxRetries: number;\n readonly error: string | null;\n readonly errorCode: AppError['code'] | null;\n readonly nextRetryAt: number | null;\n readonly createdAt: number;\n readonly meta: Meta | null;\n}\n\n/** A PendingOperation snapshot paired with its key, for iteration. */\nexport interface PendingEntry<K extends string | number, Meta = unknown>\n extends PendingOperation<Meta> {\n readonly id: K;\n}\n\n/** Mutable internal state for a pending operation. */\ninterface InternalOp<K, Meta> {\n id: K;\n operation: string;\n execute: (signal: AbortSignal) => Promise<void>;\n status: PendingOperation['status'];\n attempts: number;\n error: string | null;\n errorCode: AppError['code'] | null;\n nextRetryAt: number | null;\n createdAt: number;\n abortController: AbortController;\n retryTimer: ReturnType<typeof setTimeout> | null;\n meta: Meta | null;\n}\n\n// ── Pending ───────────────────────────────────────────────────────\n\n/**\n * Per-item operation queue with retry and status tracking.\n * Tracks operations by key with exponential backoff retry on transient errors.\n * Subscribable — auto-tracked when used as a ViewModel/Resource property.\n */\nexport class Pending<K extends string | number = string | number, Meta = unknown> {\n // ── Static config (Channel pattern — override via subclass) ──\n\n /** Maximum number of retry attempts before marking as failed. */\n static MAX_RETRIES = 5;\n /** Base delay (ms) for retry backoff. */\n static RETRY_BASE = 1000;\n /** Maximum delay cap (ms) for retry backoff. */\n static RETRY_MAX = 30000;\n /** Exponential backoff multiplier for retry delay. */\n static RETRY_FACTOR = 2;\n\n // ── Private state ──\n\n private _operations = new Map<K, InternalOp<K, Meta>>();\n private _snapshots = new Map<K, PendingOperation<Meta>>();\n private _listeners = new Set<() => void>();\n private _disposed = false;\n private _entriesCache: readonly PendingEntry<K, Meta>[] | null = null;\n\n constructor() {\n bindPublicMethods(this);\n }\n\n // ── Readable state (reactive — auto-tracked by ViewModel getters) ──\n\n /** Get the frozen status snapshot for an operation by ID, or null if not found. */\n getStatus(id: K): PendingOperation<Meta> | null {\n return this._snapshots.get(id) ?? null;\n }\n\n /** Whether an operation exists for the given ID. */\n has(id: K): boolean {\n return this._operations.has(id);\n }\n\n /** Number of operations (all statuses). */\n get count(): number {\n return this._operations.size;\n }\n\n /** Whether any operations are in-flight (active or retrying). */\n get hasPending(): boolean {\n for (const op of this._operations.values()) {\n if (op.status !== 'failed') return true;\n }\n return false;\n }\n\n /** Whether any operations are in a failed state. */\n get hasFailed(): boolean {\n for (const op of this._operations.values()) {\n if (op.status === 'failed') return true;\n }\n return false;\n }\n\n /** Number of operations in a failed state. */\n get failedCount(): number {\n let n = 0;\n for (const op of this._operations.values()) {\n if (op.status === 'failed') n++;\n }\n return n;\n }\n\n /** All operations as a frozen array of entries (id + snapshot). Cached until next mutation. */\n get entries(): readonly PendingEntry<K, Meta>[] {\n if (this._entriesCache === null) {\n const result: PendingEntry<K, Meta>[] = [];\n for (const [id, snapshot] of this._snapshots) {\n result.push(Object.freeze({ ...snapshot, id }));\n }\n this._entriesCache = Object.freeze(result) as readonly PendingEntry<K, Meta>[];\n }\n return this._entriesCache;\n }\n\n // ── Core API ──\n\n /**\n * Enqueue an operation for the given ID. Fire-and-forget (synchronous return).\n * If the same ID already has a pending operation, it is superseded (aborted).\n */\n enqueue(id: K, operation: string, execute: (signal: AbortSignal) => Promise<void>, meta?: Meta): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.enqueue() called after dispose — ignored.');\n }\n return;\n }\n\n // Supersede existing operation for this ID\n const existing = this._operations.get(id);\n if (existing) {\n existing.abortController.abort();\n if (existing.retryTimer !== null) {\n clearTimeout(existing.retryTimer);\n }\n }\n\n const op: InternalOp<K, Meta> = {\n id,\n operation,\n execute,\n status: 'active',\n attempts: 0,\n error: null,\n errorCode: null,\n nextRetryAt: null,\n createdAt: Date.now(),\n abortController: new AbortController(),\n retryTimer: null,\n meta: meta ?? null,\n };\n\n this._operations.set(id, op);\n this._snapshot(op);\n this._notify();\n\n // Schedule processing via microtask (allows batching multiple enqueues)\n queueMicrotask(() => this._process(id));\n }\n\n // ── Controls ──\n\n /** Retry a failed operation. No-op if the operation is not in 'failed' status. */\n retry(id: K): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.retry() called after dispose — ignored.');\n }\n return;\n }\n\n const op = this._operations.get(id);\n if (!op || op.status !== 'failed') return;\n\n op.attempts = 0;\n op.error = null;\n op.errorCode = null;\n op.nextRetryAt = null;\n op.abortController = new AbortController();\n this._process(id);\n }\n\n /** Retry all failed operations. */\n retryAll(): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.retryAll() called after dispose — ignored.');\n }\n return;\n }\n\n const failedIds: K[] = [];\n for (const op of this._operations.values()) {\n if (op.status === 'failed') failedIds.push(op.id);\n }\n for (const id of failedIds) {\n this.retry(id);\n }\n }\n\n /** Cancel an in-flight operation by ID. Aborts the signal, clears timers, and removes it. */\n cancel(id: K): void {\n const op = this._operations.get(id);\n if (!op) return;\n\n op.abortController.abort();\n if (op.retryTimer !== null) {\n clearTimeout(op.retryTimer);\n }\n this._operations.delete(id);\n this._snapshots.delete(id);\n this._notify();\n }\n\n /** Cancel all operations. */\n cancelAll(): void {\n for (const op of this._operations.values()) {\n op.abortController.abort();\n if (op.retryTimer !== null) {\n clearTimeout(op.retryTimer);\n }\n }\n this._operations.clear();\n this._snapshots.clear();\n if (this._listeners.size > 0) this._notify();\n }\n\n /** Remove a failed operation without retrying. No-op if the operation is not in 'failed' status. */\n dismiss(id: K): void {\n const op = this._operations.get(id);\n if (!op || op.status !== 'failed') return;\n\n this._operations.delete(id);\n this._snapshots.delete(id);\n this._notify();\n }\n\n /** Remove all failed operations without retrying. */\n dismissAll(): void {\n const failedIds: K[] = [];\n for (const op of this._operations.values()) {\n if (op.status === 'failed') failedIds.push(op.id);\n }\n if (failedIds.length === 0) return;\n for (const id of failedIds) {\n this._operations.delete(id);\n this._snapshots.delete(id);\n }\n this._notify();\n }\n\n // ── Hooks (overridable in subclass) ──\n\n /**\n * Determines whether an error is retryable. Override in a subclass to customize.\n * Default: retries on network, timeout, and server_error codes.\n * @protected\n */\n protected isRetryable(error: unknown): boolean {\n const code = classifyError(error).code;\n return code === 'network' || code === 'timeout' || code === 'server_error';\n }\n\n /** Called when an operation succeeds. Override in a subclass for side effects. @protected */\n protected onConfirmed?(id: K, operation: string): void;\n /** Called when an operation fails permanently. Override in a subclass for side effects. @protected */\n protected onFailed?(id: K, operation: string, error: unknown): void;\n\n // ── Subscribable interface (duck-typed — auto-tracked by ViewModel) ──\n\n /** Subscribe to state changes. Returns an unsubscribe function. */\n subscribe(cb: () => void): () => void {\n this._listeners.add(cb);\n return () => { this._listeners.delete(cb); };\n }\n\n // ── Lifecycle ──\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Dispose: cancels all operations, clears listeners. */\n dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n this.cancelAll();\n this._listeners.clear();\n }\n\n // ── Internals ──\n\n private _snapshot(op: InternalOp<K, Meta>): void {\n const ctor = this.constructor as typeof Pending;\n this._snapshots.set(op.id, Object.freeze({\n status: op.status,\n operation: op.operation,\n attempts: op.attempts,\n maxRetries: ctor.MAX_RETRIES,\n error: op.error,\n errorCode: op.errorCode,\n nextRetryAt: op.nextRetryAt,\n createdAt: op.createdAt,\n meta: op.meta,\n }));\n }\n\n private _notify(): void {\n this._entriesCache = null;\n for (const cb of this._listeners) cb();\n }\n\n private _process(id: K): void {\n const op = this._operations.get(id);\n if (!op || this._disposed) return;\n\n // Set active status\n op.status = 'active';\n op.attempts++;\n op.error = null;\n op.errorCode = null;\n op.nextRetryAt = null;\n this._snapshot(op);\n this._notify();\n\n op.execute(op.abortController.signal).then(\n () => {\n // Identity check: ignore if this op was superseded or cancelled\n if (this._operations.get(id) !== op) return;\n const operation = op.operation;\n this._operations.delete(id);\n this._snapshots.delete(id);\n this._notify();\n this.onConfirmed?.(id, operation);\n },\n (error: unknown) => {\n // Identity check: ignore if this op was superseded or cancelled\n if (this._operations.get(id) !== op) return;\n\n // AbortError — remove silently (cancel or supersede)\n if (isAbortError(error)) {\n this._operations.delete(id);\n this._snapshots.delete(id);\n this._notify();\n return;\n }\n\n const ctor = this.constructor as typeof Pending;\n const classified = classifyError(error);\n\n // Check if retryable and under max retries\n if (this.isRetryable(error) && op.attempts < ctor.MAX_RETRIES) {\n op.status = 'retrying';\n const delay = this._calculateDelay(op.attempts - 1);\n op.nextRetryAt = Date.now() + delay;\n op.error = classified.message;\n op.errorCode = classified.code;\n this._snapshot(op);\n this._notify();\n\n op.retryTimer = setTimeout(() => {\n op.retryTimer = null;\n this._process(id);\n }, delay);\n } else {\n // Non-retryable or max retries exceeded\n op.status = 'failed';\n op.error = classified.message;\n op.errorCode = classified.code;\n op.nextRetryAt = null;\n this._snapshot(op);\n this._notify();\n this.onFailed?.(id, op.operation, error);\n }\n },\n );\n }\n\n /** Computes retry backoff delay with jitter (Channel formula). @private */\n private _calculateDelay(attempt: number): number {\n const ctor = this.constructor as typeof Pending;\n const capped = Math.min(\n ctor.RETRY_BASE * Math.pow(ctor.RETRY_FACTOR, attempt),\n ctor.RETRY_MAX,\n );\n return Math.random() * capped;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Pending.js","sources":["../src/Pending.ts"],"sourcesContent":["import { classifyError, isAbortError } from './errors';\nimport type { AppError } from './errors';\nimport { Trackable } from './Trackable';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Types ─────────────────────────────────────────────────────────\n\n/** Frozen snapshot of a single pending operation's state. */\nexport interface PendingOperation<Meta = unknown> {\n readonly status: 'active' | 'retrying' | 'failed';\n readonly operation: string;\n readonly attempts: number;\n readonly maxRetries: number;\n readonly error: string | null;\n readonly errorCode: AppError['code'] | null;\n readonly nextRetryAt: number | null;\n readonly createdAt: number;\n readonly meta: Meta | null;\n}\n\n/** A PendingOperation snapshot paired with its key, for iteration. */\nexport interface PendingEntry<K extends string | number, Meta = unknown>\n extends PendingOperation<Meta> {\n readonly id: K;\n}\n\n/** Mutable internal state for a pending operation. */\ninterface InternalOp<K, Meta> {\n id: K;\n operation: string;\n execute: (signal: AbortSignal) => Promise<void>;\n status: PendingOperation['status'];\n attempts: number;\n error: string | null;\n errorCode: AppError['code'] | null;\n nextRetryAt: number | null;\n createdAt: number;\n abortController: AbortController;\n retryTimer: ReturnType<typeof setTimeout> | null;\n meta: Meta | null;\n}\n\n// ── Pending ───────────────────────────────────────────────────────\n\n/**\n * Per-item operation queue with retry and status tracking.\n * Tracks operations by key with exponential backoff retry on transient errors.\n * Subscribable — auto-tracked when used as a ViewModel/Resource property.\n */\nexport class Pending<K extends string | number = string | number, Meta = unknown> extends Trackable {\n // ── Static config (Channel pattern — override via subclass) ──\n\n /** Maximum number of retry attempts before marking as failed. */\n static MAX_RETRIES = 5;\n /** Base delay (ms) for retry backoff. */\n static RETRY_BASE = 1000;\n /** Maximum delay cap (ms) for retry backoff. */\n static RETRY_MAX = 30000;\n /** Exponential backoff multiplier for retry delay. */\n static RETRY_FACTOR = 2;\n\n // ── Private state ──\n\n private _operations = new Map<K, InternalOp<K, Meta>>();\n private _snapshots = new Map<K, PendingOperation<Meta>>();\n private _entriesCache: readonly PendingEntry<K, Meta>[] | null = null;\n\n constructor() {\n super();\n }\n\n // ── Readable state (reactive — auto-tracked by ViewModel getters) ──\n\n /** Get the frozen status snapshot for an operation by ID, or null if not found. */\n getStatus(id: K): PendingOperation<Meta> | null {\n return this._snapshots.get(id) ?? null;\n }\n\n /** Whether an operation exists for the given ID. */\n has(id: K): boolean {\n return this._operations.has(id);\n }\n\n /** Number of operations (all statuses). */\n get count(): number {\n return this._operations.size;\n }\n\n /** Whether any operations are in-flight (active or retrying). */\n get hasPending(): boolean {\n for (const op of this._operations.values()) {\n if (op.status !== 'failed') return true;\n }\n return false;\n }\n\n /** Whether any operations are in a failed state. */\n get hasFailed(): boolean {\n for (const op of this._operations.values()) {\n if (op.status === 'failed') return true;\n }\n return false;\n }\n\n /** Number of operations in a failed state. */\n get failedCount(): number {\n let n = 0;\n for (const op of this._operations.values()) {\n if (op.status === 'failed') n++;\n }\n return n;\n }\n\n /** All operations as a frozen array of entries (id + snapshot). Cached until next mutation. */\n get entries(): readonly PendingEntry<K, Meta>[] {\n if (this._entriesCache === null) {\n const result: PendingEntry<K, Meta>[] = [];\n for (const [id, snapshot] of this._snapshots) {\n result.push(Object.freeze({ ...snapshot, id }));\n }\n this._entriesCache = Object.freeze(result) as readonly PendingEntry<K, Meta>[];\n }\n return this._entriesCache;\n }\n\n // ── Core API ──\n\n /**\n * Enqueue an operation for the given ID. Fire-and-forget (synchronous return).\n * If the same ID already has a pending operation, it is superseded (aborted).\n */\n enqueue(id: K, operation: string, execute: (signal: AbortSignal) => Promise<void>, meta?: Meta): void {\n if (this.disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.enqueue() called after dispose — ignored.');\n }\n return;\n }\n\n // Supersede existing operation for this ID\n const existing = this._operations.get(id);\n if (existing) {\n existing.abortController.abort();\n if (existing.retryTimer !== null) {\n clearTimeout(existing.retryTimer);\n }\n }\n\n const op: InternalOp<K, Meta> = {\n id,\n operation,\n execute,\n status: 'active',\n attempts: 0,\n error: null,\n errorCode: null,\n nextRetryAt: null,\n createdAt: Date.now(),\n abortController: new AbortController(),\n retryTimer: null,\n meta: meta ?? null,\n };\n\n this._operations.set(id, op);\n this._snapshot(op);\n this.notify();\n\n // Schedule processing via microtask (allows batching multiple enqueues)\n queueMicrotask(() => this._process(id));\n }\n\n // ── Controls ──\n\n /** Retry a failed operation. No-op if the operation is not in 'failed' status. */\n retry(id: K): void {\n if (this.disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.retry() called after dispose — ignored.');\n }\n return;\n }\n\n const op = this._operations.get(id);\n if (!op || op.status !== 'failed') return;\n\n op.attempts = 0;\n op.error = null;\n op.errorCode = null;\n op.nextRetryAt = null;\n op.abortController = new AbortController();\n this._process(id);\n }\n\n /** Retry all failed operations. */\n retryAll(): void {\n if (this.disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.retryAll() called after dispose — ignored.');\n }\n return;\n }\n\n const failedIds: K[] = [];\n for (const op of this._operations.values()) {\n if (op.status === 'failed') failedIds.push(op.id);\n }\n for (const id of failedIds) {\n this.retry(id);\n }\n }\n\n /** Cancel an in-flight operation by ID. Aborts the signal, clears timers, and removes it. */\n cancel(id: K): void {\n const op = this._operations.get(id);\n if (!op) return;\n\n op.abortController.abort();\n if (op.retryTimer !== null) {\n clearTimeout(op.retryTimer);\n }\n this._operations.delete(id);\n this._snapshots.delete(id);\n this.notify();\n }\n\n /** Cancel all operations. */\n cancelAll(): void {\n for (const op of this._operations.values()) {\n op.abortController.abort();\n if (op.retryTimer !== null) {\n clearTimeout(op.retryTimer);\n }\n }\n this._operations.clear();\n this._snapshots.clear();\n this.notify();\n }\n\n /** Remove a failed operation without retrying. No-op if the operation is not in 'failed' status. */\n dismiss(id: K): void {\n const op = this._operations.get(id);\n if (!op || op.status !== 'failed') return;\n\n this._operations.delete(id);\n this._snapshots.delete(id);\n this.notify();\n }\n\n /** Remove all failed operations without retrying. */\n dismissAll(): void {\n const failedIds: K[] = [];\n for (const op of this._operations.values()) {\n if (op.status === 'failed') failedIds.push(op.id);\n }\n if (failedIds.length === 0) return;\n for (const id of failedIds) {\n this._operations.delete(id);\n this._snapshots.delete(id);\n }\n this.notify();\n }\n\n // ── Hooks (overridable in subclass) ──\n\n /**\n * Determines whether an error is retryable. Override in a subclass to customize.\n * Default: retries on network, timeout, and server_error codes.\n * @protected\n */\n protected isRetryable(error: unknown): boolean {\n const code = classifyError(error).code;\n return code === 'network' || code === 'timeout' || code === 'server_error';\n }\n\n /** Called when an operation succeeds. Override in a subclass for side effects. @protected */\n protected onConfirmed?(id: K, operation: string): void;\n /** Called when an operation fails permanently. Override in a subclass for side effects. @protected */\n protected onFailed?(id: K, operation: string, error: unknown): void;\n\n // ── Lifecycle ──\n\n /** Dispose: cancels all operations, then runs Trackable cleanup. */\n dispose(): void {\n if (this.disposed) return;\n this.cancelAll();\n super.dispose();\n }\n\n // ── Notification override ──\n\n /** @internal Invalidates entries cache before notifying subscribers. */\n protected notify(): void {\n this._entriesCache = null;\n super.notify();\n }\n\n // ── Internals ──\n\n private _snapshot(op: InternalOp<K, Meta>): void {\n const ctor = this.constructor as typeof Pending;\n this._snapshots.set(op.id, Object.freeze({\n status: op.status,\n operation: op.operation,\n attempts: op.attempts,\n maxRetries: ctor.MAX_RETRIES,\n error: op.error,\n errorCode: op.errorCode,\n nextRetryAt: op.nextRetryAt,\n createdAt: op.createdAt,\n meta: op.meta,\n }));\n }\n\n private _process(id: K): void {\n const op = this._operations.get(id);\n if (!op || this.disposed) return;\n\n // Set active status\n op.status = 'active';\n op.attempts++;\n op.error = null;\n op.errorCode = null;\n op.nextRetryAt = null;\n this._snapshot(op);\n this.notify();\n\n op.execute(op.abortController.signal).then(\n () => {\n // Identity check: ignore if this op was superseded or cancelled\n if (this._operations.get(id) !== op) return;\n const operation = op.operation;\n this._operations.delete(id);\n this._snapshots.delete(id);\n this.notify();\n this.onConfirmed?.(id, operation);\n },\n (error: unknown) => {\n // Identity check: ignore if this op was superseded or cancelled\n if (this._operations.get(id) !== op) return;\n\n // AbortError — remove silently (cancel or supersede)\n if (isAbortError(error)) {\n this._operations.delete(id);\n this._snapshots.delete(id);\n this.notify();\n return;\n }\n\n const ctor = this.constructor as typeof Pending;\n const classified = classifyError(error);\n\n // Check if retryable and under max retries\n if (this.isRetryable(error) && op.attempts < ctor.MAX_RETRIES) {\n op.status = 'retrying';\n const delay = this._calculateDelay(op.attempts - 1);\n op.nextRetryAt = Date.now() + delay;\n op.error = classified.message;\n op.errorCode = classified.code;\n this._snapshot(op);\n this.notify();\n\n op.retryTimer = setTimeout(() => {\n op.retryTimer = null;\n this._process(id);\n }, delay);\n } else {\n // Non-retryable or max retries exceeded\n op.status = 'failed';\n op.error = classified.message;\n op.errorCode = classified.code;\n op.nextRetryAt = null;\n this._snapshot(op);\n this.notify();\n this.onFailed?.(id, op.operation, error);\n }\n },\n );\n }\n\n /** Computes retry backoff delay with jitter (Channel formula). @private */\n private _calculateDelay(attempt: number): number {\n const ctor = this.constructor as typeof Pending;\n const capped = Math.min(\n ctor.RETRY_BASE * Math.pow(ctor.RETRY_FACTOR, attempt),\n ctor.RETRY_MAX,\n );\n return Math.random() * capped;\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/Selection.cjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
4
|
-
class Selection {
|
|
3
|
+
const Trackable = require("./Trackable.cjs");
|
|
4
|
+
class Selection extends Trackable.Trackable {
|
|
5
5
|
_selected = /* @__PURE__ */ new Set();
|
|
6
|
-
_listeners = /* @__PURE__ */ new Set();
|
|
7
6
|
_readonlyView = this._selected;
|
|
8
7
|
constructor() {
|
|
9
|
-
|
|
8
|
+
super();
|
|
10
9
|
}
|
|
11
10
|
// ── Readable state ──
|
|
12
11
|
/** Read-only view of currently selected keys. */
|
|
@@ -86,17 +85,10 @@ class Selection {
|
|
|
86
85
|
selectedFrom(items, keyOf) {
|
|
87
86
|
return items.filter((item) => this._selected.has(keyOf(item)));
|
|
88
87
|
}
|
|
89
|
-
// ──
|
|
90
|
-
/** Subscribe to selection changes. Returns an unsubscribe function. */
|
|
91
|
-
subscribe(cb) {
|
|
92
|
-
this._listeners.add(cb);
|
|
93
|
-
return () => {
|
|
94
|
-
this._listeners.delete(cb);
|
|
95
|
-
};
|
|
96
|
-
}
|
|
88
|
+
// ── Internal ──
|
|
97
89
|
_publish() {
|
|
98
90
|
this._readonlyView = new Set(this._selected);
|
|
99
|
-
|
|
91
|
+
this.notify();
|
|
100
92
|
}
|
|
101
93
|
}
|
|
102
94
|
exports.Selection = Selection;
|
package/dist/Selection.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Selection.cjs","sources":["../src/Selection.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Selection.cjs","sources":["../src/Selection.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/**\n * Key-based selection set with toggle and select-all support.\n * Tracks which items are selected by their key (id).\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Selection<K extends string | number = string | number> extends Trackable {\n private _selected: Set<K> = new Set();\n private _readonlyView: ReadonlySet<K> = this._selected;\n\n constructor() {\n super();\n }\n\n // ── Readable state ──\n\n /** Read-only view of currently selected keys. */\n get selected(): ReadonlySet<K> {\n return this._readonlyView;\n }\n\n /** Number of currently selected items. */\n get count(): number {\n return this._selected.size;\n }\n\n /** Whether any items are currently selected. */\n get hasSelection(): boolean {\n return this._selected.size > 0;\n }\n\n // ── Query ──\n\n /** Check whether a specific key is selected. */\n isSelected(key: K): boolean {\n return this._selected.has(key);\n }\n\n // ── Actions ──\n\n /** Toggle a key's selection state (select if unselected, deselect if selected). */\n toggle(key: K): void {\n if (this._selected.has(key)) {\n this._selected.delete(key);\n } else {\n this._selected.add(key);\n }\n this._publish();\n }\n\n /** Add one or more keys to the selection. */\n select(...keys: K[]): void {\n let changed = false;\n for (const key of keys) {\n if (!this._selected.has(key)) {\n this._selected.add(key);\n changed = true;\n }\n }\n if (changed) this._publish();\n }\n\n /** Remove one or more keys from the selection. */\n deselect(...keys: K[]): void {\n let changed = false;\n for (const key of keys) {\n if (this._selected.has(key)) {\n this._selected.delete(key);\n changed = true;\n }\n }\n if (changed) this._publish();\n }\n\n /** If all selected → deselect all, else select all. */\n toggleAll(allKeys: K[]): void {\n const allSelected = allKeys.length > 0 && allKeys.every(k => this._selected.has(k));\n if (allSelected) {\n this._selected.clear();\n } else {\n for (const key of allKeys) this._selected.add(key);\n }\n this._publish();\n }\n\n /** Replace the entire selection atomically. Single notification. */\n set(...keys: K[]): void {\n // Check if anything actually changed\n if (keys.length === this._selected.size && keys.every(k => this._selected.has(k))) return;\n this._selected.clear();\n for (const key of keys) this._selected.add(key);\n this._publish();\n }\n\n /** Remove all items from the selection. */\n clear(): void {\n if (this._selected.size === 0) return;\n this._selected.clear();\n this._publish();\n }\n\n // ── Utility ──\n\n /** Filter an array to only items whose key is in the selection. */\n selectedFrom<T>(items: T[], keyOf: (item: T) => K): T[] {\n return items.filter(item => this._selected.has(keyOf(item)));\n }\n\n // ── Internal ──\n\n private _publish(): void {\n // Replace readonlyView so reference equality changes (needed for React)\n this._readonlyView = new Set(this._selected);\n this.notify();\n }\n}\n"],"names":["Trackable"],"mappings":";;;AAOO,MAAM,kBAA+DA,UAAAA,UAAU;AAAA,EAC5E,gCAAwB,IAAA;AAAA,EACxB,gBAAgC,KAAK;AAAA,EAE7C,cAAc;AACZ,UAAA;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,IAAI,WAA2B;AAC7B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,eAAwB;AAC1B,WAAO,KAAK,UAAU,OAAO;AAAA,EAC/B;AAAA;AAAA;AAAA,EAKA,WAAW,KAAiB;AAC1B,WAAO,KAAK,UAAU,IAAI,GAAG;AAAA,EAC/B;AAAA;AAAA;AAAA,EAKA,OAAO,KAAc;AACnB,QAAI,KAAK,UAAU,IAAI,GAAG,GAAG;AAC3B,WAAK,UAAU,OAAO,GAAG;AAAA,IAC3B,OAAO;AACL,WAAK,UAAU,IAAI,GAAG;AAAA,IACxB;AACA,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,UAAU,MAAiB;AACzB,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,KAAK,UAAU,IAAI,GAAG,GAAG;AAC5B,aAAK,UAAU,IAAI,GAAG;AACtB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,cAAc,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,YAAY,MAAiB;AAC3B,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,UAAI,KAAK,UAAU,IAAI,GAAG,GAAG;AAC3B,aAAK,UAAU,OAAO,GAAG;AACzB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,cAAc,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,UAAU,SAAoB;AAC5B,UAAM,cAAc,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAA,MAAK,KAAK,UAAU,IAAI,CAAC,CAAC;AAClF,QAAI,aAAa;AACf,WAAK,UAAU,MAAA;AAAA,IACjB,OAAO;AACL,iBAAW,OAAO,QAAS,MAAK,UAAU,IAAI,GAAG;AAAA,IACnD;AACA,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,OAAO,MAAiB;AAEtB,QAAI,KAAK,WAAW,KAAK,UAAU,QAAQ,KAAK,MAAM,CAAA,MAAK,KAAK,UAAU,IAAI,CAAC,CAAC,EAAG;AACnF,SAAK,UAAU,MAAA;AACf,eAAW,OAAO,KAAM,MAAK,UAAU,IAAI,GAAG;AAC9C,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,SAAS,EAAG;AAC/B,SAAK,UAAU,MAAA;AACf,SAAK,SAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,aAAgB,OAAY,OAA4B;AACtD,WAAO,MAAM,OAAO,CAAA,SAAQ,KAAK,UAAU,IAAI,MAAM,IAAI,CAAC,CAAC;AAAA,EAC7D;AAAA;AAAA,EAIQ,WAAiB;AAEvB,SAAK,gBAAgB,IAAI,IAAI,KAAK,SAAS;AAC3C,SAAK,OAAA;AAAA,EACP;AACF;;"}
|
package/dist/Selection.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { Trackable } from './Trackable';
|
|
1
2
|
/**
|
|
2
3
|
* Key-based selection set with toggle and select-all support.
|
|
3
4
|
* Tracks which items are selected by their key (id).
|
|
4
5
|
* Subscribable — auto-tracked when used as a ViewModel property.
|
|
5
6
|
*/
|
|
6
|
-
export declare class Selection<K extends string | number = string | number> {
|
|
7
|
+
export declare class Selection<K extends string | number = string | number> extends Trackable {
|
|
7
8
|
private _selected;
|
|
8
|
-
private _listeners;
|
|
9
9
|
private _readonlyView;
|
|
10
10
|
constructor();
|
|
11
11
|
/** Read-only view of currently selected keys. */
|
|
@@ -30,8 +30,6 @@ export declare class Selection<K extends string | number = string | number> {
|
|
|
30
30
|
clear(): void;
|
|
31
31
|
/** Filter an array to only items whose key is in the selection. */
|
|
32
32
|
selectedFrom<T>(items: T[], keyOf: (item: T) => K): T[];
|
|
33
|
-
/** Subscribe to selection changes. Returns an unsubscribe function. */
|
|
34
|
-
subscribe(cb: () => void): () => void;
|
|
35
33
|
private _publish;
|
|
36
34
|
}
|
|
37
35
|
//# sourceMappingURL=Selection.d.ts.map
|
package/dist/Selection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Selection.d.ts","sourceRoot":"","sources":["../src/Selection.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Selection.d.ts","sourceRoot":"","sources":["../src/Selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;;GAIG;AACH,qBAAa,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAE,SAAQ,SAAS;IACnF,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,aAAa,CAAkC;;IAQvD,iDAAiD;IACjD,IAAI,QAAQ,IAAI,WAAW,CAAC,CAAC,CAAC,CAE7B;IAED,0CAA0C;IAC1C,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,gDAAgD;IAChD,IAAI,YAAY,IAAI,OAAO,CAE1B;IAID,gDAAgD;IAChD,UAAU,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO;IAM3B,mFAAmF;IACnF,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI;IASpB,6CAA6C;IAC7C,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI;IAW1B,kDAAkD;IAClD,QAAQ,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI;IAW5B,uDAAuD;IACvD,SAAS,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI;IAU7B,oEAAoE;IACpE,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI;IAQvB,2CAA2C;IAC3C,KAAK,IAAI,IAAI;IAQb,mEAAmE;IACnE,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;IAMvD,OAAO,CAAC,QAAQ;CAKjB"}
|
package/dist/Selection.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
class Selection {
|
|
1
|
+
import { Trackable } from "./Trackable.js";
|
|
2
|
+
class Selection extends Trackable {
|
|
3
3
|
_selected = /* @__PURE__ */ new Set();
|
|
4
|
-
_listeners = /* @__PURE__ */ new Set();
|
|
5
4
|
_readonlyView = this._selected;
|
|
6
5
|
constructor() {
|
|
7
|
-
|
|
6
|
+
super();
|
|
8
7
|
}
|
|
9
8
|
// ── Readable state ──
|
|
10
9
|
/** Read-only view of currently selected keys. */
|
|
@@ -84,17 +83,10 @@ class Selection {
|
|
|
84
83
|
selectedFrom(items, keyOf) {
|
|
85
84
|
return items.filter((item) => this._selected.has(keyOf(item)));
|
|
86
85
|
}
|
|
87
|
-
// ──
|
|
88
|
-
/** Subscribe to selection changes. Returns an unsubscribe function. */
|
|
89
|
-
subscribe(cb) {
|
|
90
|
-
this._listeners.add(cb);
|
|
91
|
-
return () => {
|
|
92
|
-
this._listeners.delete(cb);
|
|
93
|
-
};
|
|
94
|
-
}
|
|
86
|
+
// ── Internal ──
|
|
95
87
|
_publish() {
|
|
96
88
|
this._readonlyView = new Set(this._selected);
|
|
97
|
-
|
|
89
|
+
this.notify();
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
92
|
export {
|
package/dist/Selection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Selection.js","sources":["../src/Selection.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Selection.js","sources":["../src/Selection.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/**\n * Key-based selection set with toggle and select-all support.\n * Tracks which items are selected by their key (id).\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Selection<K extends string | number = string | number> extends Trackable {\n private _selected: Set<K> = new Set();\n private _readonlyView: ReadonlySet<K> = this._selected;\n\n constructor() {\n super();\n }\n\n // ── Readable state ──\n\n /** Read-only view of currently selected keys. */\n get selected(): ReadonlySet<K> {\n return this._readonlyView;\n }\n\n /** Number of currently selected items. */\n get count(): number {\n return this._selected.size;\n }\n\n /** Whether any items are currently selected. */\n get hasSelection(): boolean {\n return this._selected.size > 0;\n }\n\n // ── Query ──\n\n /** Check whether a specific key is selected. */\n isSelected(key: K): boolean {\n return this._selected.has(key);\n }\n\n // ── Actions ──\n\n /** Toggle a key's selection state (select if unselected, deselect if selected). */\n toggle(key: K): void {\n if (this._selected.has(key)) {\n this._selected.delete(key);\n } else {\n this._selected.add(key);\n }\n this._publish();\n }\n\n /** Add one or more keys to the selection. */\n select(...keys: K[]): void {\n let changed = false;\n for (const key of keys) {\n if (!this._selected.has(key)) {\n this._selected.add(key);\n changed = true;\n }\n }\n if (changed) this._publish();\n }\n\n /** Remove one or more keys from the selection. */\n deselect(...keys: K[]): void {\n let changed = false;\n for (const key of keys) {\n if (this._selected.has(key)) {\n this._selected.delete(key);\n changed = true;\n }\n }\n if (changed) this._publish();\n }\n\n /** If all selected → deselect all, else select all. */\n toggleAll(allKeys: K[]): void {\n const allSelected = allKeys.length > 0 && allKeys.every(k => this._selected.has(k));\n if (allSelected) {\n this._selected.clear();\n } else {\n for (const key of allKeys) this._selected.add(key);\n }\n this._publish();\n }\n\n /** Replace the entire selection atomically. Single notification. */\n set(...keys: K[]): void {\n // Check if anything actually changed\n if (keys.length === this._selected.size && keys.every(k => this._selected.has(k))) return;\n this._selected.clear();\n for (const key of keys) this._selected.add(key);\n this._publish();\n }\n\n /** Remove all items from the selection. */\n clear(): void {\n if (this._selected.size === 0) return;\n this._selected.clear();\n this._publish();\n }\n\n // ── Utility ──\n\n /** Filter an array to only items whose key is in the selection. */\n selectedFrom<T>(items: T[], keyOf: (item: T) => K): T[] {\n return items.filter(item => this._selected.has(keyOf(item)));\n }\n\n // ── Internal ──\n\n private _publish(): void {\n // Replace readonlyView so reference equality changes (needed for React)\n this._readonlyView = new Set(this._selected);\n this.notify();\n }\n}\n"],"names":[],"mappings":";AAOO,MAAM,kBAA+D,UAAU;AAAA,EAC5E,gCAAwB,IAAA;AAAA,EACxB,gBAAgC,KAAK;AAAA,EAE7C,cAAc;AACZ,UAAA;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,IAAI,WAA2B;AAC7B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,eAAwB;AAC1B,WAAO,KAAK,UAAU,OAAO;AAAA,EAC/B;AAAA;AAAA;AAAA,EAKA,WAAW,KAAiB;AAC1B,WAAO,KAAK,UAAU,IAAI,GAAG;AAAA,EAC/B;AAAA;AAAA;AAAA,EAKA,OAAO,KAAc;AACnB,QAAI,KAAK,UAAU,IAAI,GAAG,GAAG;AAC3B,WAAK,UAAU,OAAO,GAAG;AAAA,IAC3B,OAAO;AACL,WAAK,UAAU,IAAI,GAAG;AAAA,IACxB;AACA,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,UAAU,MAAiB;AACzB,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,KAAK,UAAU,IAAI,GAAG,GAAG;AAC5B,aAAK,UAAU,IAAI,GAAG;AACtB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,cAAc,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,YAAY,MAAiB;AAC3B,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,UAAI,KAAK,UAAU,IAAI,GAAG,GAAG;AAC3B,aAAK,UAAU,OAAO,GAAG;AACzB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,cAAc,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,UAAU,SAAoB;AAC5B,UAAM,cAAc,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAA,MAAK,KAAK,UAAU,IAAI,CAAC,CAAC;AAClF,QAAI,aAAa;AACf,WAAK,UAAU,MAAA;AAAA,IACjB,OAAO;AACL,iBAAW,OAAO,QAAS,MAAK,UAAU,IAAI,GAAG;AAAA,IACnD;AACA,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,OAAO,MAAiB;AAEtB,QAAI,KAAK,WAAW,KAAK,UAAU,QAAQ,KAAK,MAAM,CAAA,MAAK,KAAK,UAAU,IAAI,CAAC,CAAC,EAAG;AACnF,SAAK,UAAU,MAAA;AACf,eAAW,OAAO,KAAM,MAAK,UAAU,IAAI,GAAG;AAC9C,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,SAAS,EAAG;AAC/B,SAAK,UAAU,MAAA;AACf,SAAK,SAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,aAAgB,OAAY,OAA4B;AACtD,WAAO,MAAM,OAAO,CAAA,SAAQ,KAAK,UAAU,IAAI,MAAM,IAAI,CAAC,CAAC;AAAA,EAC7D;AAAA;AAAA,EAIQ,WAAiB;AAEvB,SAAK,gBAAgB,IAAI,IAAI,KAAK,SAAS;AAC3C,SAAK,OAAA;AAAA,EACP;AACF;"}
|
package/dist/Sorting.cjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
4
|
-
class Sorting {
|
|
3
|
+
const Trackable = require("./Trackable.cjs");
|
|
4
|
+
class Sorting extends Trackable.Trackable {
|
|
5
5
|
_sorts;
|
|
6
|
-
_listeners = /* @__PURE__ */ new Set();
|
|
7
6
|
constructor(options) {
|
|
7
|
+
super();
|
|
8
8
|
this._sorts = Object.freeze(options?.sorts?.map((s) => ({ ...s })) ?? []);
|
|
9
|
-
bindPublicMethods.bindPublicMethods(this);
|
|
10
9
|
}
|
|
11
10
|
// ── Readable state ──
|
|
12
11
|
/** Current list of active sort descriptors, in priority order. */
|
|
@@ -49,22 +48,22 @@ class Sorting {
|
|
|
49
48
|
} else {
|
|
50
49
|
this._sorts = Object.freeze(this._sorts.filter((_, i) => i !== idx));
|
|
51
50
|
}
|
|
52
|
-
this.
|
|
51
|
+
this.notify();
|
|
53
52
|
}
|
|
54
53
|
/** Replace all with a single sort. */
|
|
55
54
|
setSort(key, direction) {
|
|
56
55
|
this._sorts = Object.freeze([{ key, direction }]);
|
|
57
|
-
this.
|
|
56
|
+
this.notify();
|
|
58
57
|
}
|
|
59
58
|
/** Replace all sorts. */
|
|
60
59
|
setSorts(sorts) {
|
|
61
60
|
this._sorts = Object.freeze(sorts.map((s) => ({ ...s })));
|
|
62
|
-
this.
|
|
61
|
+
this.notify();
|
|
63
62
|
}
|
|
64
63
|
/** Clear all sort descriptors. */
|
|
65
64
|
reset() {
|
|
66
65
|
this._sorts = Object.freeze([]);
|
|
67
|
-
this.
|
|
66
|
+
this.notify();
|
|
68
67
|
}
|
|
69
68
|
// ── Pipeline ──
|
|
70
69
|
/** Sort an array using the current descriptors. Returns a new sorted array. */
|
|
@@ -87,17 +86,6 @@ class Sorting {
|
|
|
87
86
|
});
|
|
88
87
|
return sorted;
|
|
89
88
|
}
|
|
90
|
-
// ── Subscribable interface ──
|
|
91
|
-
/** Subscribe to sort state changes. Returns an unsubscribe function. */
|
|
92
|
-
subscribe(cb) {
|
|
93
|
-
this._listeners.add(cb);
|
|
94
|
-
return () => {
|
|
95
|
-
this._listeners.delete(cb);
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
_notify() {
|
|
99
|
-
for (const cb of this._listeners) cb();
|
|
100
|
-
}
|
|
101
89
|
}
|
|
102
90
|
function defaultCompare(a, b, key) {
|
|
103
91
|
const aVal = a[key];
|
package/dist/Sorting.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sorting.cjs","sources":["../src/Sorting.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Sorting.cjs","sources":["../src/Sorting.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/** Describes a single sort column with key and direction. */\nexport interface SortDescriptor {\n key: string;\n direction: 'asc' | 'desc';\n}\n\n/**\n * Multi-column sort state manager with a comparator pipeline.\n * Maintains an ordered list of sort descriptors and applies them to arrays.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Sorting<T = any> extends Trackable {\n private _sorts: readonly SortDescriptor[];\n\n constructor(options?: { sorts?: SortDescriptor[] }) {\n super();\n this._sorts = Object.freeze(options?.sorts?.map(s => ({ ...s })) ?? []);\n }\n\n // ── Readable state ──\n\n /** Current list of active sort descriptors, in priority order. */\n get sorts(): readonly SortDescriptor[] {\n return this._sorts;\n }\n\n /** Primary sort key (first descriptor), or null when empty. */\n get key(): string | null {\n return this._sorts.length > 0 ? this._sorts[0].key : null;\n }\n\n /** Primary sort direction. Defaults to 'asc' when empty. */\n get direction(): 'asc' | 'desc' {\n return this._sorts.length > 0 ? this._sorts[0].direction : 'asc';\n }\n\n // ── Query ──\n\n /** Whether the given key is currently sorted. */\n isSorted(key: string): boolean {\n return this._sorts.some(s => s.key === key);\n }\n\n /** Returns the sort direction for a key, or null if not sorted. */\n directionOf(key: string): 'asc' | 'desc' | null {\n const found = this._sorts.find(s => s.key === key);\n return found ? found.direction : null;\n }\n\n /** Returns the priority index of a sorted key, or -1 if not sorted. */\n indexOf(key: string): number {\n return this._sorts.findIndex(s => s.key === key);\n }\n\n // ── Actions ──\n\n /** 3-click cycle: not sorted → asc → desc → removed. */\n toggle(key: string): void {\n const idx = this.indexOf(key);\n if (idx === -1) {\n // Add as asc\n this._sorts = Object.freeze([...this._sorts, { key, direction: 'asc' as const }]);\n } else if (this._sorts[idx].direction === 'asc') {\n // Flip to desc\n const next = this._sorts.map((s, i) =>\n i === idx ? { key: s.key, direction: 'desc' as const } : s\n );\n this._sorts = Object.freeze(next);\n } else {\n // Remove\n this._sorts = Object.freeze(this._sorts.filter((_, i) => i !== idx));\n }\n this.notify();\n }\n\n /** Replace all with a single sort. */\n setSort(key: string, direction: 'asc' | 'desc'): void {\n this._sorts = Object.freeze([{ key, direction }]);\n this.notify();\n }\n\n /** Replace all sorts. */\n setSorts(sorts: SortDescriptor[]): void {\n this._sorts = Object.freeze(sorts.map(s => ({ ...s })));\n this.notify();\n }\n\n /** Clear all sort descriptors. */\n reset(): void {\n this._sorts = Object.freeze([]);\n this.notify();\n }\n\n // ── Pipeline ──\n\n /** Sort an array using the current descriptors. Returns a new sorted array. */\n apply(\n items: T[],\n compareFn?: (a: T, b: T, key: string, dir: 'asc' | 'desc') => number,\n ): T[] {\n if (this._sorts.length === 0) return items;\n const sorted = items.slice();\n const sorts = this._sorts;\n sorted.sort((a, b) => {\n for (const { key, direction } of sorts) {\n let cmp: number;\n if (compareFn) {\n cmp = compareFn(a, b, key, direction);\n } else {\n cmp = defaultCompare(a, b, key);\n }\n if (direction === 'desc') cmp = -cmp;\n if (cmp !== 0) return cmp;\n }\n return 0;\n });\n return sorted;\n }\n}\n\nfunction defaultCompare(a: any, b: any, key: string): number {\n const aVal = a[key];\n const bVal = b[key];\n if (aVal == null && bVal == null) return 0;\n if (aVal == null) return -1;\n if (bVal == null) return 1;\n if (typeof aVal === 'string' && typeof bVal === 'string') {\n return aVal.localeCompare(bVal);\n }\n if (aVal < bVal) return -1;\n if (aVal > bVal) return 1;\n return 0;\n}\n"],"names":["Trackable"],"mappings":"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"}
|
package/dist/Sorting.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Trackable } from './Trackable';
|
|
1
2
|
/** Describes a single sort column with key and direction. */
|
|
2
3
|
export interface SortDescriptor {
|
|
3
4
|
key: string;
|
|
@@ -8,9 +9,8 @@ export interface SortDescriptor {
|
|
|
8
9
|
* Maintains an ordered list of sort descriptors and applies them to arrays.
|
|
9
10
|
* Subscribable — auto-tracked when used as a ViewModel property.
|
|
10
11
|
*/
|
|
11
|
-
export declare class Sorting<T = any> {
|
|
12
|
+
export declare class Sorting<T = any> extends Trackable {
|
|
12
13
|
private _sorts;
|
|
13
|
-
private _listeners;
|
|
14
14
|
constructor(options?: {
|
|
15
15
|
sorts?: SortDescriptor[];
|
|
16
16
|
});
|
|
@@ -36,8 +36,5 @@ export declare class Sorting<T = any> {
|
|
|
36
36
|
reset(): void;
|
|
37
37
|
/** Sort an array using the current descriptors. Returns a new sorted array. */
|
|
38
38
|
apply(items: T[], compareFn?: (a: T, b: T, key: string, dir: 'asc' | 'desc') => number): T[];
|
|
39
|
-
/** Subscribe to sort state changes. Returns an unsubscribe function. */
|
|
40
|
-
subscribe(cb: () => void): () => void;
|
|
41
|
-
private _notify;
|
|
42
39
|
}
|
|
43
40
|
//# sourceMappingURL=Sorting.d.ts.map
|
package/dist/Sorting.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sorting.d.ts","sourceRoot":"","sources":["../src/Sorting.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Sorting.d.ts","sourceRoot":"","sources":["../src/Sorting.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,OAAO,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,SAAS;IAC7C,OAAO,CAAC,MAAM,CAA4B;gBAE9B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,cAAc,EAAE,CAAA;KAAE;IAOlD,kEAAkE;IAClE,IAAI,KAAK,IAAI,SAAS,cAAc,EAAE,CAErC;IAED,+DAA+D;IAC/D,IAAI,GAAG,IAAI,MAAM,GAAG,IAAI,CAEvB;IAED,4DAA4D;IAC5D,IAAI,SAAS,IAAI,KAAK,GAAG,MAAM,CAE9B;IAID,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI9B,mEAAmE;IACnE,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI;IAK/C,uEAAuE;IACvE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAM5B,wDAAwD;IACxD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAkBzB,sCAAsC;IACtC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI;IAKrD,yBAAyB;IACzB,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,IAAI;IAKvC,kCAAkC;IAClC,KAAK,IAAI,IAAI;IAOb,+EAA+E;IAC/E,KAAK,CACH,KAAK,EAAE,CAAC,EAAE,EACV,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,GAAG,MAAM,KAAK,MAAM,GACnE,CAAC,EAAE;CAmBP"}
|
package/dist/Sorting.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
class Sorting {
|
|
1
|
+
import { Trackable } from "./Trackable.js";
|
|
2
|
+
class Sorting extends Trackable {
|
|
3
3
|
_sorts;
|
|
4
|
-
_listeners = /* @__PURE__ */ new Set();
|
|
5
4
|
constructor(options) {
|
|
5
|
+
super();
|
|
6
6
|
this._sorts = Object.freeze(options?.sorts?.map((s) => ({ ...s })) ?? []);
|
|
7
|
-
bindPublicMethods(this);
|
|
8
7
|
}
|
|
9
8
|
// ── Readable state ──
|
|
10
9
|
/** Current list of active sort descriptors, in priority order. */
|
|
@@ -47,22 +46,22 @@ class Sorting {
|
|
|
47
46
|
} else {
|
|
48
47
|
this._sorts = Object.freeze(this._sorts.filter((_, i) => i !== idx));
|
|
49
48
|
}
|
|
50
|
-
this.
|
|
49
|
+
this.notify();
|
|
51
50
|
}
|
|
52
51
|
/** Replace all with a single sort. */
|
|
53
52
|
setSort(key, direction) {
|
|
54
53
|
this._sorts = Object.freeze([{ key, direction }]);
|
|
55
|
-
this.
|
|
54
|
+
this.notify();
|
|
56
55
|
}
|
|
57
56
|
/** Replace all sorts. */
|
|
58
57
|
setSorts(sorts) {
|
|
59
58
|
this._sorts = Object.freeze(sorts.map((s) => ({ ...s })));
|
|
60
|
-
this.
|
|
59
|
+
this.notify();
|
|
61
60
|
}
|
|
62
61
|
/** Clear all sort descriptors. */
|
|
63
62
|
reset() {
|
|
64
63
|
this._sorts = Object.freeze([]);
|
|
65
|
-
this.
|
|
64
|
+
this.notify();
|
|
66
65
|
}
|
|
67
66
|
// ── Pipeline ──
|
|
68
67
|
/** Sort an array using the current descriptors. Returns a new sorted array. */
|
|
@@ -85,17 +84,6 @@ class Sorting {
|
|
|
85
84
|
});
|
|
86
85
|
return sorted;
|
|
87
86
|
}
|
|
88
|
-
// ── Subscribable interface ──
|
|
89
|
-
/** Subscribe to sort state changes. Returns an unsubscribe function. */
|
|
90
|
-
subscribe(cb) {
|
|
91
|
-
this._listeners.add(cb);
|
|
92
|
-
return () => {
|
|
93
|
-
this._listeners.delete(cb);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
_notify() {
|
|
97
|
-
for (const cb of this._listeners) cb();
|
|
98
|
-
}
|
|
99
87
|
}
|
|
100
88
|
function defaultCompare(a, b, key) {
|
|
101
89
|
const aVal = a[key];
|
package/dist/Sorting.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sorting.js","sources":["../src/Sorting.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Sorting.js","sources":["../src/Sorting.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/** Describes a single sort column with key and direction. */\nexport interface SortDescriptor {\n key: string;\n direction: 'asc' | 'desc';\n}\n\n/**\n * Multi-column sort state manager with a comparator pipeline.\n * Maintains an ordered list of sort descriptors and applies them to arrays.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Sorting<T = any> extends Trackable {\n private _sorts: readonly SortDescriptor[];\n\n constructor(options?: { sorts?: SortDescriptor[] }) {\n super();\n this._sorts = Object.freeze(options?.sorts?.map(s => ({ ...s })) ?? []);\n }\n\n // ── Readable state ──\n\n /** Current list of active sort descriptors, in priority order. */\n get sorts(): readonly SortDescriptor[] {\n return this._sorts;\n }\n\n /** Primary sort key (first descriptor), or null when empty. */\n get key(): string | null {\n return this._sorts.length > 0 ? this._sorts[0].key : null;\n }\n\n /** Primary sort direction. Defaults to 'asc' when empty. */\n get direction(): 'asc' | 'desc' {\n return this._sorts.length > 0 ? this._sorts[0].direction : 'asc';\n }\n\n // ── Query ──\n\n /** Whether the given key is currently sorted. */\n isSorted(key: string): boolean {\n return this._sorts.some(s => s.key === key);\n }\n\n /** Returns the sort direction for a key, or null if not sorted. */\n directionOf(key: string): 'asc' | 'desc' | null {\n const found = this._sorts.find(s => s.key === key);\n return found ? found.direction : null;\n }\n\n /** Returns the priority index of a sorted key, or -1 if not sorted. */\n indexOf(key: string): number {\n return this._sorts.findIndex(s => s.key === key);\n }\n\n // ── Actions ──\n\n /** 3-click cycle: not sorted → asc → desc → removed. */\n toggle(key: string): void {\n const idx = this.indexOf(key);\n if (idx === -1) {\n // Add as asc\n this._sorts = Object.freeze([...this._sorts, { key, direction: 'asc' as const }]);\n } else if (this._sorts[idx].direction === 'asc') {\n // Flip to desc\n const next = this._sorts.map((s, i) =>\n i === idx ? { key: s.key, direction: 'desc' as const } : s\n );\n this._sorts = Object.freeze(next);\n } else {\n // Remove\n this._sorts = Object.freeze(this._sorts.filter((_, i) => i !== idx));\n }\n this.notify();\n }\n\n /** Replace all with a single sort. */\n setSort(key: string, direction: 'asc' | 'desc'): void {\n this._sorts = Object.freeze([{ key, direction }]);\n this.notify();\n }\n\n /** Replace all sorts. */\n setSorts(sorts: SortDescriptor[]): void {\n this._sorts = Object.freeze(sorts.map(s => ({ ...s })));\n this.notify();\n }\n\n /** Clear all sort descriptors. */\n reset(): void {\n this._sorts = Object.freeze([]);\n this.notify();\n }\n\n // ── Pipeline ──\n\n /** Sort an array using the current descriptors. Returns a new sorted array. */\n apply(\n items: T[],\n compareFn?: (a: T, b: T, key: string, dir: 'asc' | 'desc') => number,\n ): T[] {\n if (this._sorts.length === 0) return items;\n const sorted = items.slice();\n const sorts = this._sorts;\n sorted.sort((a, b) => {\n for (const { key, direction } of sorts) {\n let cmp: number;\n if (compareFn) {\n cmp = compareFn(a, b, key, direction);\n } else {\n cmp = defaultCompare(a, b, key);\n }\n if (direction === 'desc') cmp = -cmp;\n if (cmp !== 0) return cmp;\n }\n return 0;\n });\n return sorted;\n }\n}\n\nfunction defaultCompare(a: any, b: any, key: string): number {\n const aVal = a[key];\n const bVal = b[key];\n if (aVal == null && bVal == null) return 0;\n if (aVal == null) return -1;\n if (bVal == null) return 1;\n if (typeof aVal === 'string' && typeof bVal === 'string') {\n return aVal.localeCompare(bVal);\n }\n if (aVal < bVal) return -1;\n if (aVal > bVal) return 1;\n return 0;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const bindPublicMethods = require("./bindPublicMethods.cjs");
|
|
4
|
+
const PROTECTED_KEYS = /* @__PURE__ */ new Set(["addCleanup", "notify"]);
|
|
5
|
+
class Trackable {
|
|
6
|
+
_listeners = null;
|
|
7
|
+
_disposed = false;
|
|
8
|
+
_abortController = null;
|
|
9
|
+
_cleanups = null;
|
|
10
|
+
constructor() {
|
|
11
|
+
bindPublicMethods.bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
12
|
+
}
|
|
13
|
+
// ── Disposable interface ──
|
|
14
|
+
/** Whether this instance has been disposed. */
|
|
15
|
+
get disposed() {
|
|
16
|
+
return this._disposed;
|
|
17
|
+
}
|
|
18
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
19
|
+
get disposeSignal() {
|
|
20
|
+
if (!this._abortController) {
|
|
21
|
+
this._abortController = new AbortController();
|
|
22
|
+
}
|
|
23
|
+
return this._abortController.signal;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Tear down the instance: abort the dispose signal, run all registered
|
|
27
|
+
* cleanups, clear subscribers, and call onDispose. Idempotent.
|
|
28
|
+
*/
|
|
29
|
+
dispose() {
|
|
30
|
+
if (this._disposed) return;
|
|
31
|
+
this._disposed = true;
|
|
32
|
+
this._abortController?.abort();
|
|
33
|
+
if (this._cleanups) {
|
|
34
|
+
for (const fn of this._cleanups) fn();
|
|
35
|
+
this._cleanups = null;
|
|
36
|
+
}
|
|
37
|
+
this._listeners?.clear();
|
|
38
|
+
this.onDispose?.();
|
|
39
|
+
}
|
|
40
|
+
// ── Subscribable (notification-only, no state) ──
|
|
41
|
+
/**
|
|
42
|
+
* Subscribe to change notifications. The callback is invoked (with no
|
|
43
|
+
* arguments) whenever the subclass calls {@link notify}.
|
|
44
|
+
*
|
|
45
|
+
* This is the duck-typed contract that ViewModel's auto-tracking system
|
|
46
|
+
* recognizes — any object with a `subscribe` method is automatically
|
|
47
|
+
* tracked when accessed inside a ViewModel getter.
|
|
48
|
+
*
|
|
49
|
+
* @returns An unsubscribe function.
|
|
50
|
+
*/
|
|
51
|
+
subscribe(cb) {
|
|
52
|
+
if (!this._listeners) this._listeners = /* @__PURE__ */ new Set();
|
|
53
|
+
this._listeners.add(cb);
|
|
54
|
+
return () => {
|
|
55
|
+
this._listeners?.delete(cb);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Notify all subscribers that state has changed. Call this after
|
|
60
|
+
* mutating internal state to trigger ViewModel getter invalidation
|
|
61
|
+
* and React re-renders.
|
|
62
|
+
* @protected
|
|
63
|
+
*/
|
|
64
|
+
notify() {
|
|
65
|
+
if (this._listeners) {
|
|
66
|
+
for (const cb of this._listeners) cb();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ── Protected utilities ──
|
|
70
|
+
/**
|
|
71
|
+
* Register a cleanup function to be called on {@link dispose}.
|
|
72
|
+
* Cleanups run in registration order.
|
|
73
|
+
* @protected
|
|
74
|
+
*/
|
|
75
|
+
addCleanup(fn) {
|
|
76
|
+
if (!this._cleanups) this._cleanups = [];
|
|
77
|
+
this._cleanups.push(fn);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.Trackable = Trackable;
|
|
81
|
+
//# sourceMappingURL=Trackable.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Trackable.cjs","sources":["../src/Trackable.ts"],"sourcesContent":["import type { Disposable } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst PROTECTED_KEYS = new Set(['addCleanup', 'notify']);\n\n/**\n * Base class for custom reactive objects that integrate with ViewModel's\n * auto-tracking system. Provides subscribable notifications, disposal\n * lifecycle, and automatic method binding.\n *\n * Any object with a `subscribe()` method is auto-tracked by ViewModel\n * getters. Trackable gives you that plus cleanup infrastructure and\n * point-free methods — the same building blocks used by Sorting,\n * Selection, Feed, Pagination, and Pending.\n *\n * Use Trackable when integrating third-party SDKs, custom query objects,\n * or any reactive state that doesn't fit ViewModel's state/getter model.\n *\n * @example\n * ```ts\n * class RPCQuery<Data> extends Trackable {\n * private _data: Data | undefined;\n * private _loading = false;\n *\n * get data() { return this._data; }\n * get loading() { return this._loading; }\n *\n * async call(): Promise<void> {\n * this._loading = true;\n * this.notify();\n * this._data = await fetchData();\n * this._loading = false;\n * this.notify();\n * }\n * }\n *\n * // Used as a ViewModel property — auto-tracked:\n * class UsersVM extends ViewModel {\n * readonly users = new RPCQuery<User[]>();\n * get userList() { return this.users.data ?? []; }\n * }\n * ```\n */\nexport class Trackable implements Disposable {\n private _listeners: Set<() => void> | null = null;\n private _disposed = false;\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n constructor() {\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\n\n // ── Disposable interface ──\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Tear down the instance: abort the dispose signal, run all registered\n * cleanups, clear subscribers, and call onDispose. Idempotent.\n */\n dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this._listeners?.clear();\n this.onDispose?.();\n }\n\n // ── Subscribable (notification-only, no state) ──\n\n /**\n * Subscribe to change notifications. The callback is invoked (with no\n * arguments) whenever the subclass calls {@link notify}.\n *\n * This is the duck-typed contract that ViewModel's auto-tracking system\n * recognizes — any object with a `subscribe` method is automatically\n * tracked when accessed inside a ViewModel getter.\n *\n * @returns An unsubscribe function.\n */\n subscribe(cb: () => void): () => void {\n if (!this._listeners) this._listeners = new Set();\n this._listeners.add(cb);\n return () => { this._listeners?.delete(cb); };\n }\n\n /**\n * Notify all subscribers that state has changed. Call this after\n * mutating internal state to trigger ViewModel getter invalidation\n * and React re-renders.\n * @protected\n */\n protected notify(): void {\n if (this._listeners) {\n for (const cb of this._listeners) cb();\n }\n }\n\n // ── Protected utilities ──\n\n /**\n * Register a cleanup function to be called on {@link dispose}.\n * Cleanups run in registration order.\n * @protected\n */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) this._cleanups = [];\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called at the end of dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":["bindPublicMethods"],"mappings":";;;AAGA,MAAM,iBAAiB,oBAAI,IAAI,CAAC,cAAc,QAAQ,CAAC;AAwChD,MAAM,UAAgC;AAAA,EACnC,aAAqC;AAAA,EACrC,YAAY;AAAA,EACZ,mBAA2C;AAAA,EAC3C,YAAmC;AAAA,EAE3C,cAAc;AACZA,sBAAAA,kBAAkB,MAAM,OAAO,WAAW,cAAc;AAAA,EAC1D;AAAA;AAAA;AAAA,EAKA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAgB;AACd,QAAI,KAAK,UAAW;AACpB,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAY,MAAA;AACjB,SAAK,YAAA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,UAAU,IAA4B;AACpC,QAAI,CAAC,KAAK,WAAY,MAAK,iCAAiB,IAAA;AAC5C,SAAK,WAAW,IAAI,EAAE;AACtB,WAAO,MAAM;AAAE,WAAK,YAAY,OAAO,EAAE;AAAA,IAAG;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,SAAe;AACvB,QAAI,KAAK,YAAY;AACnB,iBAAW,MAAM,KAAK,WAAY,IAAA;AAAA,IACpC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,UAAW,MAAK,YAAY,CAAA;AACtC,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAIF;;"}
|