@weiliang0121/capix-view 0.1.0 → 0.3.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 CHANGED
@@ -14,6 +14,46 @@ npm install @weiliang0121/capix-view
14
14
 
15
15
  This package is part of CAPiX. The npm distribution contains compiled JavaScript, type declarations, and this package's MIT license. Reference services and client applications are not included in this package or the initial npm release.
16
16
 
17
+ ## Adapting author-owned data
18
+
19
+ `createViewSource` optionally implements the existing synchronous source contract from four callbacks. It does not create an entity, own a container, persist data, or manage a resource access window.
20
+
21
+ ```ts
22
+ import { createViewSource, View } from '@weiliang0121/capix-view';
23
+
24
+ const documents = new Map<string, { title: string; text: string }>([['guide', { title: 'Guide', text: 'Read this first.' }]]);
25
+ let revision = 0;
26
+ const requireDocument = (key: string) => {
27
+ if (!documents.has(key)) throw new RangeError('Document is missing or outside scope');
28
+ return documents.get(key)!;
29
+ };
30
+ const source = createViewSource({
31
+ id: 'local-documents',
32
+ scope: 'whole',
33
+ version: () => String(revision),
34
+ keys: () => [...documents.keys()],
35
+ inspect: (key) => ({ title: requireDocument(key).title }),
36
+ read: (key) => requireDocument(key),
37
+ });
38
+ const sources = { documents: source };
39
+ const view = new View<typeof sources>({ revision: 0, sources: { documents: { id: source.id, scope: source.scope } }, members: [{ source: 'documents', key: 'guide' }] });
40
+ const index = view.indexSync(sources);
41
+ const snapshot = view.captureSync(sources, index);
42
+
43
+ documents.set('guide', { title: 'Guide', text: 'Updated instructions.' });
44
+ revision++; // Every observable source change requires a new, never-reused version.
45
+ ```
46
+
47
+ The adapter fixes `id`, `scope`, and callback selection at construction, preserving each callback's original receiver. `version()` must cover directory order, membership, summaries, and values for that identity and scope. It must never reuse an earlier version. The adapter compares stamps before callbacks and after result isolation; it rejects changes and reentrant source operations without retrying. It cannot detect an author changing content without changing its version.
48
+
49
+ `keys()` supplies the complete directory for this scope. Keys must be nonempty strings or nonnegative safe integers, with no duplicates. `index()` validates this directory and calls `inspect(key)` for each entry; it never reads bodies. `inspect(keys)` calls only the selected metadata callbacks, and `read(keys, expected)` calls only the selected body callbacks after checking the expected stamp. Neither selected operation calls `keys()`, and reading does not call `inspect()`. Directory work scales with the directory; selected work scales with the requested keys and returned data.
50
+
51
+ Authors must check missing keys and scope in both `inspect(key)` and `read(key)` and throw `RangeError` when access is invalid. Key shape validation is not a scope or authorization check. `undefined` is a valid summary or body, so it cannot indicate a missing key. All callbacks must finish synchronously; Promise and thenable results are rejected, and rejected asynchronous results are observed.
52
+
53
+ Summaries and bodies must be passive scalar values, dense arrays, or ordinary data records. The adapter detaches and recursively freezes returned data without freezing the source, configuration, or author-owned values. Accessors, proxies, live objects, functions, cycles, and unsupported scalar values are rejected. `ViewSourceData<T>` describes the readonly result shape; it grants no runtime `Stable` evidence, persistence format, or recovery guarantee.
54
+
55
+ Custom `SyncViewSource` and asynchronous `ViewSource` implementations remain valid. No source identity interface or new source kind is required for those implementations. A resource adapter still uses `openView` or `openViewManager` to enforce its access window; `createViewSource` alone provides no resource permission or lifecycle.
56
+
17
57
  ## License
18
58
 
19
- MIT. Copyright (c) 2026 wei.liang. See [LICENSE](./LICENSE).
59
+ MIT. Copyright (c) 2026 wei.liang. See [LICENSE](LICENSE).
@@ -0,0 +1,13 @@
1
+ import type { SourceIdentity, SyncViewSource, ViewKey } from './source.js';
2
+ export type ViewSourceData<Value> = unknown extends Value ? unknown : Value extends (...args: never[]) => unknown ? never : Value extends readonly unknown[] ? {
3
+ readonly [Key in keyof Value]: ViewSourceData<Value[Key]>;
4
+ } : Value extends object ? {
5
+ readonly [Key in keyof Value]: ViewSourceData<Value[Key]>;
6
+ } : Value extends undefined | null | boolean | number | string ? Value : never;
7
+ export interface ViewSourceOptions<Key extends ViewKey, Summary, Value> extends SourceIdentity {
8
+ version(): string;
9
+ keys(): readonly Key[];
10
+ inspect(key: Key): Summary & ViewSourceData<Summary>;
11
+ read(key: Key): Value & ViewSourceData<Value>;
12
+ }
13
+ export declare function createViewSource<Key extends ViewKey, Summary, Value>(options: ViewSourceOptions<Key, Summary, Value>): SyncViewSource<Key, ViewSourceData<Summary>, ViewSourceData<Value>> & SourceIdentity;
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{checkIdentity,checkKey,checkStamp,conflict,data,object,sameStamp,synchronous}from"./value.js";function createViewSource(options){object(options);const identity=data({id:options.id,scope:options.scope});checkIdentity(identity);const versionMethod=options.version,keysMethod=options.keys,inspectMethod=options.inspect,readMethod=options.read;if(typeof versionMethod!="function"||typeof keysMethod!="function"||typeof inspectMethod!="function"||typeof readMethod!="function")throw new TypeError("Invalid view source callbacks");const version=versionMethod.bind(options),keys=keysMethod.bind(options),inspect=inspectMethod.bind(options),read=readMethod.bind(options),current=__name(()=>{const stamp=data({...identity,version:synchronous(version(),"Source version")});return checkStamp(stamp),stamp},"current");let reading=!1;const access=__name((prepare,operation)=>{reading&&conflict("View source access cannot reenter"),reading=!0;try{const input=prepare(),stamp=current(),result=operation(stamp,input);return sameStamp(stamp,current())||conflict("View source version has changed"),result}finally{reading=!1}},"access"),selected=__name(value=>{const accepted=data(value);if(!Array.isArray(accepted))throw new TypeError("View source keys must be an array");const seen=new Set;for(const key of accepted){if(checkKey(key),seen.has(key))throw new TypeError("View source keys must be unique");seen.add(key)}return accepted},"selected"),expectedStamp=__name(value=>{const accepted=data(value);return checkStamp(accepted),accepted},"expectedStamp"),summaries=__name(selected2=>Object.freeze(selected2.map(key=>Object.freeze({key,summary:data(synchronous(inspect(key),"Source summary"))}))),"summaries");return Object.freeze({...identity,index:__name(()=>access(()=>{},stamp=>Object.freeze({stamp,entries:summaries(selected(synchronous(keys(),"Source keys")))})),"index"),inspect:__name(requested=>access(()=>selected(requested),(stamp,input)=>Object.freeze({stamp,entries:summaries(input)})),"inspect"),read:__name((requested,expected)=>access(()=>({keys:selected(requested),expected:expectedStamp(expected)}),(stamp,input)=>{sameStamp(stamp,input.expected)||conflict("View source version has changed");const entries=input.keys.map(key=>Object.freeze({key,value:data(synchronous(read(key),"Source value"))}));return Object.freeze({stamp,entries:Object.freeze(entries)})}),"read"),check:__name(expected=>access(()=>expectedStamp(expected),(stamp,input)=>sameStamp(stamp,input)),"check")})}__name(createViewSource,"createViewSource");export{createViewSource};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { View } from './view.js';
2
+ export { createViewSource, type ViewSourceData, type ViewSourceOptions } from './adapter.js';
2
3
  export { openView, openViewManager, type ViewAccess, type ViewManager } from './access.js';
3
4
  export type { Awaitable, ViewKey, SourceIdentity, SourceStamp, SourceIndex, SourceInspection, SourceSnapshot, ViewSource, SyncViewSource, ViewSources, SyncViewSources, SourceBindings, SyncSourceBindings, SourceKey, SourceSummary, SourceValue } from './source.js';
4
5
  export type { ViewReference, ViewIndexEntry, ViewEntry, ViewState, ViewBasis, ViewIndex, ViewSnapshot, ViewSelection, ViewCommit, ViewSourceChecks, ViewPersistence } from './types.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
- import{View}from"./view.js";import{openView,openViewManager}from"./access.js";export{View,openView,openViewManager};
2
+ import{View}from"./view.js";import{createViewSource}from"./adapter.js";import{openView,openViewManager}from"./access.js";export{View,createViewSource,openView,openViewManager};
package/dist/value.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
- var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{types}from"node:util";import{RuntimeError}from"@weiliang0121/capix-contracts";function conflict(message){throw new RuntimeError("VIEW_CONFLICT",message)}__name(conflict,"conflict");function data(value){const visiting=new Set,copy=__name(input=>{if(input==null||typeof input=="string"||typeof input=="boolean"||typeof input=="number"&&Number.isFinite(input))return input;if(typeof input!="object"||types.isProxy(input))throw new TypeError("View content must be passive data");if(visiting.has(input))throw new TypeError("View content cannot contain cycles");const array=Array.isArray(input);if(!array&&Object.getPrototypeOf(input)!==Object.prototype&&Object.getPrototypeOf(input)!==null)throw new TypeError("View content cannot contain live objects");visiting.add(input);try{const result=array?[]:{};let reusable=Object.isFrozen(input);for(const key of Reflect.ownKeys(input)){if(array&&key==="length")continue;if(typeof key!="string")throw new TypeError("View content cannot contain symbol fields");const descriptor=Object.getOwnPropertyDescriptor(input,key);if(!("value"in descriptor)||!descriptor.enumerable)throw new TypeError("View content must use enumerable data fields");if(array&&(!/^(0|[1-9]\d*)$/.test(key)||Number(key)>=input.length))throw new TypeError("View arrays cannot contain named fields");const copied=copy(descriptor.value);copied!==descriptor.value&&(reusable=!1),Object.defineProperty(result,key,{value:copied,enumerable:!0,configurable:!0,writable:!0})}if(array&&Object.keys(input).length!==input.length)throw new TypeError("View arrays must be dense");return reusable?input:Object.freeze(result)}finally{visiting.delete(input)}},"copy");return copy(value)}__name(data,"data");function object(value){if(!value||typeof value!="object"||Array.isArray(value))throw new TypeError("Invalid view object")}__name(object,"object");function exactKeys(value,keys){if(Object.keys(value).length!==keys.length||keys.some(key=>!Object.hasOwn(value,key)))throw new TypeError("View fields do not match the contract")}__name(exactKeys,"exactKeys");function checkIdentity(value){if(object(value),typeof value.id!="string"||!/\S/.test(value.id)||typeof value.scope!="string"||!/\S/.test(value.scope))throw new TypeError("Invalid view source identity")}__name(checkIdentity,"checkIdentity");function checkStamp(value){if(checkIdentity(value),exactKeys(value,["id","scope","version"]),typeof value.version!="string"||!value.version.length)throw new TypeError("Invalid view source version")}__name(checkStamp,"checkStamp");function sameIdentity(left,right){return left.id===right.id&&left.scope===right.scope}__name(sameIdentity,"sameIdentity");function sameStamp(left,right){return sameIdentity(left,right)&&left.version===right.version}__name(sameStamp,"sameStamp");function checkKey(key){if(!(typeof key=="string"&&key.length>0)&&!(typeof key=="number"&&Number.isSafeInteger(key)&&key>=0))throw new TypeError("View keys must be nonempty strings or nonnegative safe integers")}__name(checkKey,"checkKey");function synchronous(value,label){if(value&&(typeof value=="object"||typeof value=="function")&&typeof value.then=="function")throw Promise.resolve(value).catch(()=>{}),new TypeError(`${label} must complete synchronously`);return value}__name(synchronous,"synchronous");export{checkIdentity,checkKey,checkStamp,conflict,data,exactKeys,object,sameIdentity,sameStamp,synchronous};
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{types}from"node:util";import{RuntimeError}from"@weiliang0121/capix-contracts";function conflict(message){throw new RuntimeError("VIEW_CONFLICT",message)}__name(conflict,"conflict");function data(value){const visiting=new Set,copy=__name(input=>{if(input==null||typeof input=="string"||typeof input=="boolean"||typeof input=="number"&&Number.isFinite(input))return input;if(typeof input!="object"||types.isProxy(input))throw new TypeError("View content must be passive data");if(visiting.has(input))throw new TypeError("View content cannot contain cycles");const array=Array.isArray(input),prototype=Object.getPrototypeOf(input);if(array?prototype!==Array.prototype:prototype!==Object.prototype&&prototype!==null)throw new TypeError("View content cannot contain live objects");visiting.add(input);try{const result=array?[]:{};let reusable=Object.isFrozen(input);for(const key of Reflect.ownKeys(input)){if(array&&key==="length")continue;if(typeof key!="string")throw new TypeError("View content cannot contain symbol fields");const descriptor=Object.getOwnPropertyDescriptor(input,key);if(!("value"in descriptor)||!descriptor.enumerable)throw new TypeError("View content must use enumerable data fields");if(array&&(!/^(0|[1-9]\d*)$/.test(key)||Number(key)>=input.length))throw new TypeError("View arrays cannot contain named fields");const copied=copy(descriptor.value);copied!==descriptor.value&&(reusable=!1),Object.defineProperty(result,key,{value:copied,enumerable:!0,configurable:!0,writable:!0})}if(array&&Object.keys(input).length!==input.length)throw new TypeError("View arrays must be dense");return reusable?input:Object.freeze(result)}finally{visiting.delete(input)}},"copy");return copy(value)}__name(data,"data");function object(value){if(!value||typeof value!="object"||Array.isArray(value))throw new TypeError("Invalid view object")}__name(object,"object");function exactKeys(value,keys){if(Object.keys(value).length!==keys.length||keys.some(key=>!Object.hasOwn(value,key)))throw new TypeError("View fields do not match the contract")}__name(exactKeys,"exactKeys");function checkIdentity(value){if(object(value),typeof value.id!="string"||!/\S/.test(value.id)||typeof value.scope!="string"||!/\S/.test(value.scope))throw new TypeError("Invalid view source identity")}__name(checkIdentity,"checkIdentity");function checkStamp(value){if(checkIdentity(value),exactKeys(value,["id","scope","version"]),typeof value.version!="string"||!value.version.length)throw new TypeError("Invalid view source version")}__name(checkStamp,"checkStamp");function sameIdentity(left,right){return left.id===right.id&&left.scope===right.scope}__name(sameIdentity,"sameIdentity");function sameStamp(left,right){return sameIdentity(left,right)&&left.version===right.version}__name(sameStamp,"sameStamp");function checkKey(key){if(!(typeof key=="string"&&key.length>0)&&!(typeof key=="number"&&Number.isSafeInteger(key)&&key>=0))throw new TypeError("View keys must be nonempty strings or nonnegative safe integers")}__name(checkKey,"checkKey");function synchronous(value,label){if(value&&(typeof value=="object"||typeof value=="function")){let current=value;for(;current!==null;){if(types.isProxy(current))throw new TypeError(`${label} cannot use a proxy`);const descriptor=Object.getOwnPropertyDescriptor(current,"then");if(descriptor){if(!("value"in descriptor))throw new TypeError(`${label} cannot use a then accessor`);if(typeof descriptor.value=="function")throw Promise.resolve(value).catch(()=>{}),new TypeError(`${label} must complete synchronously`);break}current=Object.getPrototypeOf(current)}}return value}__name(synchronous,"synchronous");export{checkIdentity,checkKey,checkStamp,conflict,data,exactKeys,object,sameIdentity,sameStamp,synchronous};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weiliang0121/capix-view",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -10,9 +10,9 @@
10
10
  }
11
11
  },
12
12
  "dependencies": {
13
- "@weiliang0121/capix-contracts": "0.1.0",
14
- "@weiliang0121/capix-diagnostics": "0.1.0",
15
- "@weiliang0121/capix-resources": "0.1.0"
13
+ "@weiliang0121/capix-diagnostics": "0.3.0",
14
+ "@weiliang0121/capix-contracts": "0.3.0",
15
+ "@weiliang0121/capix-resources": "0.3.0"
16
16
  },
17
17
  "description": "Versioned views over independently owned content for CAPiX.",
18
18
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/weiliang0121/capix.git",
23
- "directory": "server/view"
23
+ "directory": "packages/view"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=22.23.2"
@@ -39,6 +39,7 @@
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.json",
42
- "typecheck": "tsc -p tsconfig.json --noEmit"
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test": "node ../../scripts/test/run.mjs --package ."
43
44
  }
44
45
  }