@weiliang0121/capix-view 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wei.liang (https://github.com/weiliang0121)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # @weiliang0121/capix-view
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Versioned views over independently owned content for CAPiX.
4
+
5
+ ES modules and TypeScript declarations. Requires Node.js 22.23.2 or newer.
6
+
7
+ ```sh
8
+ npm install @weiliang0121/capix-view
9
+ ```
10
+
11
+ ## Public entry points
12
+
13
+ - `@weiliang0121/capix-view`
14
+
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
+
17
+ ## License
18
+
19
+ MIT. Copyright (c) 2026 wei.liang. See [LICENSE](./LICENSE).
@@ -0,0 +1,21 @@
1
+ import type { ResourceEndpointActivity } from '@weiliang0121/capix-resources';
2
+ import type { SourceBindings, SyncSourceBindings, ViewSources } from './source.js';
3
+ import type { ViewIndex, ViewPersistence, ViewReference, ViewSelection, ViewSnapshot, ViewState } from './types.js';
4
+ import type { View } from './view.js';
5
+ export interface ViewAccess<Sources extends ViewSources> {
6
+ has(reference: ViewReference<Sources>): boolean;
7
+ members(): readonly ViewReference<Sources>[];
8
+ index(): Promise<ViewIndex<Sources>>;
9
+ capture(index?: ViewIndex<Sources>): Promise<ViewSnapshot<Sources>>;
10
+ }
11
+ export interface ViewManager<Sources extends ViewSources> {
12
+ has(reference: ViewReference<Sources>): boolean;
13
+ members(): readonly ViewReference<Sources>[];
14
+ include(references: readonly ViewReference<Sources>[]): ViewState<Sources>;
15
+ exclude(references: readonly ViewReference<Sources>[]): ViewState<Sources>;
16
+ index(): ViewIndex<Sources>;
17
+ capture(index?: ViewIndex<Sources>): ViewSnapshot<Sources>;
18
+ apply(selection: ViewSelection<Sources>): ViewState<Sources>;
19
+ }
20
+ export declare function openView<Sources extends ViewSources>(view: View<Sources>, sources: SourceBindings<Sources>, activity: ResourceEndpointActivity): ViewAccess<Sources>;
21
+ export declare function openViewManager<Sources extends ViewSources>(view: View<Sources>, sources: SyncSourceBindings<Sources>, activity: ResourceEndpointActivity, persistence?: ViewPersistence<Sources>): ViewManager<Sources>;
package/dist/access.js ADDED
@@ -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{sourcePorts}from"./reading.js";function openView(view,sources,activity){activity.validate();const ports=sourcePorts(sources,Object.keys(view.exportState().sources),()=>activity.validate());return Object.freeze({has:__name(reference=>activity.runSync("has",{},()=>view.has(reference)),"has"),members:__name(()=>activity.runSync("members",{output:__name(members=>({count:members.length}),"output")},()=>view.members()),"members"),index:__name(()=>activity.runAsync("index",{output:__name(index=>({revision:index.basis.viewRevision,members:index.members.length}),"output")},()=>view.index(ports)),"index"),capture:__name(index=>activity.runAsync("capture",{output:__name(snapshot=>({revision:snapshot.basis.viewRevision,members:snapshot.entries.length}),"output")},()=>view.capture(ports,index)),"capture")})}__name(openView,"openView");function openViewManager(view,sources,activity,persistence){activity.validate();const ports=sourcePorts(sources,Object.keys(view.exportState().sources),()=>activity.validate()),writer=persistence?Object.freeze({commit:persistence.commit.bind(persistence)}):void 0;return Object.freeze({has:__name(reference=>activity.runSync("has",{},()=>view.has(reference)),"has"),members:__name(()=>activity.runSync("members",{output:__name(members=>({count:members.length}),"output")},()=>view.members()),"members"),include:__name(references=>activity.runSync("include",{output:__name(state=>({revision:state.revision,members:state.members.length}),"output")},()=>view.includeSync(ports,references,writer)),"include"),exclude:__name(references=>activity.runSync("exclude",{output:__name(state=>({revision:state.revision,members:state.members.length}),"output")},()=>view.excludeSync(references,writer)),"exclude"),index:__name(()=>activity.runSync("index",{output:__name(index=>({revision:index.basis.viewRevision,members:index.members.length}),"output")},()=>view.indexSync(ports)),"index"),capture:__name(index=>activity.runSync("capture",{output:__name(snapshot=>({revision:snapshot.basis.viewRevision,members:snapshot.entries.length}),"output")},()=>view.captureSync(ports,index)),"capture"),apply:__name(selection=>activity.runSync("apply",{output:__name(state=>({revision:state.revision,members:state.members.length}),"output")},()=>view.applySync(ports,selection,writer)),"apply")})}__name(openViewManager,"openViewManager");export{openView,openViewManager};
@@ -0,0 +1,4 @@
1
+ export { View } from './view.js';
2
+ export { openView, openViewManager, type ViewAccess, type ViewManager } from './access.js';
3
+ export type { Awaitable, ViewKey, SourceIdentity, SourceStamp, SourceIndex, SourceInspection, SourceSnapshot, ViewSource, SyncViewSource, ViewSources, SyncViewSources, SourceBindings, SyncSourceBindings, SourceKey, SourceSummary, SourceValue } from './source.js';
4
+ export type { ViewReference, ViewIndexEntry, ViewEntry, ViewState, ViewBasis, ViewIndex, ViewSnapshot, ViewSelection, ViewCommit, ViewSourceChecks, ViewPersistence } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
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};
@@ -0,0 +1,7 @@
1
+ import type { ViewSources } from './source.js';
2
+ import type { ViewBasis, ViewReference, ViewState } from './types.js';
3
+ export declare function members<Sources extends ViewSources>(value: readonly ViewReference<Sources>[], slots: readonly string[], deduplicate?: boolean): readonly ViewReference<Sources>[];
4
+ export declare function memberIndex<Sources extends ViewSources>(members: readonly ViewReference<Sources>[]): ReadonlyMap<string, ReadonlySet<string | number>>;
5
+ export declare function state<Sources extends ViewSources>(value: ViewState<Sources>): ViewState<Sources>;
6
+ export declare function basis<Sources extends ViewSources>(value: ViewBasis<Sources>, current: ViewState<Sources>): ViewBasis<Sources>;
7
+ export declare function sameMembers<Sources extends ViewSources>(left: readonly ViewReference<Sources>[], right: readonly ViewReference<Sources>[]): boolean;
@@ -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,exactKeys,object,sameIdentity}from"./value.js";function members(value,slots,deduplicate=!1){if(!Array.isArray(value))throw new TypeError("View members must be an array");const seen=new Map(slots.map(slot=>[slot,new Set])),accepted=data(value),unique=accepted.filter(member=>{if(object(member),exactKeys(member,["source","key"]),typeof member.source!="string")throw new TypeError("View member source must be named");const keys=seen.get(member.source);if(!keys)throw new TypeError("View member refers to an unknown source");if(checkKey(member.key),keys.has(member.key)){if(deduplicate)return!1;throw new TypeError("View members must be unique")}return keys.add(member.key),!0});return deduplicate?Object.freeze(unique):accepted}__name(members,"members");function memberIndex(members2){const index=new Map;for(const member of members2){let keys=index.get(member.source);keys||(keys=new Set,index.set(member.source,keys)),keys.add(member.key)}return index}__name(memberIndex,"memberIndex");function state(value){const accepted=data(value);if(object(accepted),exactKeys(accepted,["revision","sources","members"]),!Number.isSafeInteger(accepted.revision)||accepted.revision<0)throw new TypeError("Invalid view revision");object(accepted.sources);const slots=Object.keys(accepted.sources);for(const slot of slots){if(!slot.length)throw new TypeError("View source slots must be named");checkIdentity(accepted.sources[slot]),exactKeys(accepted.sources[slot],["id","scope"])}return Object.freeze({revision:accepted.revision,sources:accepted.sources,members:members(accepted.members,slots)})}__name(state,"state");function basis(value,current){const accepted=data(value);object(accepted),exactKeys(accepted,["viewRevision","sources"]),accepted.viewRevision!==current.revision&&conflict("View membership has changed"),object(accepted.sources),exactKeys(accepted.sources,Object.keys(current.sources));for(const slot of Object.keys(current.sources)){const stamp=accepted.sources[slot];checkStamp(stamp),sameIdentity(stamp,current.sources[slot])||conflict("View source identity or scope has changed")}return accepted}__name(basis,"basis");function sameMembers(left,right){return left.length===right.length&&left.every((entry,index)=>entry.source===right[index].source&&entry.key===right[index].key)}__name(sameMembers,"sameMembers");export{basis,memberIndex,members,sameMembers,state};
@@ -0,0 +1,24 @@
1
+ import type { SourceBindings, SyncSourceBindings, ViewSources } from './source.js';
2
+ import type { ViewBasis, ViewIndex, ViewReference, ViewSnapshot, ViewState, ViewSourceChecks } from './types.js';
3
+ export declare function sourcePorts<Sources extends ViewSources>(sources: SyncSourceBindings<Sources>, slots: readonly string[], validate?: () => void): SyncSourceBindings<Sources>;
4
+ export declare function sourcePorts<Sources extends ViewSources>(sources: SourceBindings<Sources>, slots: readonly string[], validate?: () => void): SourceBindings<Sources>;
5
+ export declare class ViewReading<Sources extends ViewSources> {
6
+ #private;
7
+ private readonly state;
8
+ constructor(state: ViewState<Sources>, sources: SourceBindings<Sources>);
9
+ index(): Promise<ViewIndex<Sources>>;
10
+ indexSync(): ViewIndex<Sources>;
11
+ selectionSync(members: readonly ViewReference<Sources>[], expected?: ViewBasis<Sources>): ViewIndex<Sources>;
12
+ additionsSync(members: readonly ViewReference<Sources>[]): ViewSourceChecks<Sources>;
13
+ capture(basis: ViewBasis<Sources>): Promise<ViewSnapshot<Sources>>;
14
+ captureSync(basis: ViewBasis<Sources>): ViewSnapshot<Sources>;
15
+ private check;
16
+ private checkSync;
17
+ private checked;
18
+ private directory;
19
+ private snapshot;
20
+ private entries;
21
+ private project;
22
+ private keys;
23
+ private values;
24
+ }
@@ -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{checkKey,checkStamp,conflict,data,exactKeys,object,sameIdentity,sameStamp,synchronous}from"./value.js";function sourcePorts(sources,slots,validate){object(sources),exactKeys(sources,slots);const entries=slots.map(slot=>{const source=sources[slot];if(!source||typeof source.index!="function"||typeof source.inspect!="function"||typeof source.read!="function"||typeof source.check!="function")throw new TypeError("Invalid view source endpoint");const index=source.index.bind(source),inspect=source.inspect.bind(source),read=source.read.bind(source),check=source.check.bind(source),returned=__name(result=>{try{validate?.()}catch(error){throw Promise.resolve(result).catch(()=>{}),error}return result},"returned");return[slot,Object.freeze({index:__name(()=>(validate?.(),returned(index())),"index"),inspect:__name(keys=>(validate?.(),returned(inspect(keys))),"inspect"),read:__name((keys,stamp)=>(validate?.(),returned(read(keys,stamp))),"read"),check:__name(stamp=>(validate?.(),returned(check(stamp))),"check")})]});return Object.freeze(Object.fromEntries(entries))}__name(sourcePorts,"sourcePorts");class ViewReading{static{__name(this,"ViewReading")}state;#ports;#slots;constructor(state,sources){this.state=state,this.#slots=Object.keys(state.sources),this.#ports=sourcePorts(sources,this.#slots)}async index(){const directories=new Map;for(const slot of this.#slots){const keys=this.keys(slot);directories.set(slot,this.directory(slot,await this.#ports[slot].inspect(keys),keys))}const index=this.project(directories,this.state.members);return await this.check(index.basis),index}indexSync(){return this.selectionSync(this.state.members)}selectionSync(members,expected){const directories=new Map;for(const slot of this.#slots){const keys=this.keys(slot,members);directories.set(slot,this.directory(slot,synchronous(this.#ports[slot].inspect(keys),"Source inspection"),keys))}const index=this.project(directories,members);if(expected)for(const slot of this.#slots)sameStamp(index.basis.sources[slot],expected.sources[slot])||conflict("View source version has changed");return this.checkSync(index.basis),index}additionsSync(members){const stamps={};for(const slot of new Set(members.map(member=>member.source))){const keys=this.keys(slot,members),inspected=this.directory(slot,synchronous(this.#ports[slot].inspect(keys),"Source inspection"),keys);Object.defineProperty(stamps,slot,{value:inspected.stamp,enumerable:!0})}for(const[slot,stamp]of Object.entries(stamps))this.checked(synchronous(this.#ports[slot].check(stamp),"Source checking"));return Object.freeze(stamps)}async capture(basis){await this.check(basis);const snapshots=new Map;for(const slot of this.#slots){const keys=this.keys(slot);keys.length&&snapshots.set(slot,this.snapshot(slot,await this.#ports[slot].read(keys,basis.sources[slot]),basis,keys))}return await this.check(basis),this.values(basis,snapshots)}captureSync(basis){this.checkSync(basis);const snapshots=new Map;for(const slot of this.#slots){const keys=this.keys(slot);keys.length&&snapshots.set(slot,this.snapshot(slot,synchronous(this.#ports[slot].read(keys,basis.sources[slot]),"Source reading"),basis,keys))}return this.checkSync(basis),this.values(basis,snapshots)}async check(basis){for(const slot of this.#slots)this.checked(await this.#ports[slot].check(basis.sources[slot]))}checkSync(basis){for(const slot of this.#slots)this.checked(synchronous(this.#ports[slot].check(basis.sources[slot]),"Source checking"))}checked(result){if(typeof result!="boolean")throw new TypeError("Source checking must return a boolean");result||conflict("View source version has changed")}directory(slot,value,keys){const accepted=data(value);object(accepted),exactKeys(accepted,["stamp","entries"]),checkStamp(accepted.stamp),sameIdentity(accepted.stamp,this.state.sources[slot])||conflict("View source identity or scope has changed");const returned=this.entries(accepted.entries);if(returned.size!==keys.length||keys.some(key=>!returned.has(key)))throw new RangeError("Source must inspect every requested member and no others");for(const entry of accepted.entries)exactKeys(entry,["key","summary"]);return accepted}snapshot(slot,value,basis,keys){const accepted=data(value);object(accepted),exactKeys(accepted,["stamp","entries"]),checkStamp(accepted.stamp),sameStamp(accepted.stamp,basis.sources[slot])||conflict("Source returned content from another version");const returned=this.entries(accepted.entries);if(returned.size!==keys.length||keys.some(key=>!returned.has(key)))throw new RangeError("Source must return every requested member and no others");for(const entry of accepted.entries)exactKeys(entry,["key","value"]);return accepted}entries(entries){if(!Array.isArray(entries))throw new TypeError("Source entries must be an array");const keys=new Set;for(const entry of entries){if(object(entry),checkKey(entry.key),keys.has(entry.key))throw new TypeError("Source entries must be unique");keys.add(entry.key)}return keys}project(directories,members){const summaries=new Map(Array.from(directories,([slot,directory])=>[slot,new Map(directory.entries.map(entry=>[entry.key,entry.summary]))])),entries=members.map(member=>{const values=summaries.get(member.source);if(!values.has(member.key))throw new RangeError("View member is missing or outside the source scope");return Object.freeze({...member,summary:values.get(member.key)})}),stamps=Object.freeze(Object.fromEntries(Array.from(directories,([slot,directory])=>[slot,directory.stamp])));return Object.freeze({basis:Object.freeze({viewRevision:this.state.revision,sources:stamps}),members,entries:Object.freeze(entries)})}keys(slot,members=this.state.members){return Object.freeze(members.filter(member=>member.source===slot).map(member=>member.key))}values(basis,snapshots){const values=new Map(Array.from(snapshots,([slot,snapshot])=>[slot,new Map(snapshot.entries.map(entry=>[entry.key,entry.value]))])),entries=this.state.members.map(member=>Object.freeze({...member,value:values.get(member.source).get(member.key)}));return Object.freeze({basis,entries:Object.freeze(entries)})}}export{ViewReading,sourcePorts};
@@ -0,0 +1,53 @@
1
+ export interface SourceIdentity {
2
+ readonly id: string;
3
+ readonly scope: string;
4
+ }
5
+ export interface SourceStamp extends SourceIdentity {
6
+ readonly version: string;
7
+ }
8
+ export type ViewKey = string | number;
9
+ export type Awaitable<Value> = Value | PromiseLike<Value>;
10
+ export interface SourceIndex<Key extends ViewKey, Summary> {
11
+ readonly stamp: SourceStamp;
12
+ readonly entries: readonly {
13
+ readonly key: Key;
14
+ readonly summary: Summary;
15
+ }[];
16
+ }
17
+ export interface SourceInspection<Key extends ViewKey, Summary> {
18
+ readonly stamp: SourceStamp;
19
+ readonly entries: readonly {
20
+ readonly key: Key;
21
+ readonly summary: Summary;
22
+ }[];
23
+ }
24
+ export interface SourceSnapshot<Key extends ViewKey, Value> {
25
+ readonly stamp: SourceStamp;
26
+ readonly entries: readonly {
27
+ readonly key: Key;
28
+ readonly value: Value;
29
+ }[];
30
+ }
31
+ export interface ViewSource<Key extends ViewKey, Summary, Value> {
32
+ index(): Awaitable<SourceIndex<Key, Summary>>;
33
+ inspect(keys: readonly Key[]): Awaitable<SourceInspection<Key, Summary>>;
34
+ read(keys: readonly Key[], expected: SourceStamp): Awaitable<SourceSnapshot<Key, Value>>;
35
+ check(expected: SourceStamp): Awaitable<boolean>;
36
+ }
37
+ export interface SyncViewSource<Key extends ViewKey, Summary, Value> extends ViewSource<Key, Summary, Value> {
38
+ index(): SourceIndex<Key, Summary>;
39
+ inspect(keys: readonly Key[]): SourceInspection<Key, Summary>;
40
+ read(keys: readonly Key[], expected: SourceStamp): SourceSnapshot<Key, Value>;
41
+ check(expected: SourceStamp): boolean;
42
+ }
43
+ export type ViewSources = Readonly<Record<string, ViewSource<any, any, any>>>;
44
+ export type SyncViewSources = Readonly<Record<string, SyncViewSource<any, any, any>>>;
45
+ export type SourceKey<Source> = Source extends ViewSource<infer Key, any, any> ? Key : never;
46
+ export type SourceSummary<Source> = Source extends ViewSource<any, infer Summary, any> ? Summary : never;
47
+ export type SourceValue<Source> = Source extends ViewSource<any, any, infer Value> ? Value : never;
48
+ export type SourceBindings<Sources extends ViewSources> = {
49
+ readonly [Name in keyof Sources]: ViewSource<SourceKey<Sources[Name]>, SourceSummary<Sources[Name]>, SourceValue<Sources[Name]>>;
50
+ };
51
+ export type SyncSourceBindings<Sources extends ViewSources> = {
52
+ readonly [Name in keyof Sources]: SyncViewSource<SourceKey<Sources[Name]>, SourceSummary<Sources[Name]>, SourceValue<Sources[Name]>>;
53
+ };
package/dist/source.js ADDED
@@ -0,0 +1 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
@@ -0,0 +1,58 @@
1
+ import type { SourceIdentity, SourceKey, SourceStamp, SourceSummary, SourceValue, ViewSources } from './source.js';
2
+ export type ViewReference<Sources extends ViewSources> = {
3
+ [Name in keyof Sources & string]: {
4
+ readonly source: Name;
5
+ readonly key: SourceKey<Sources[Name]>;
6
+ };
7
+ }[keyof Sources & string];
8
+ export type ViewIndexEntry<Sources extends ViewSources> = {
9
+ [Name in keyof Sources & string]: {
10
+ readonly source: Name;
11
+ readonly key: SourceKey<Sources[Name]>;
12
+ readonly summary: SourceSummary<Sources[Name]>;
13
+ };
14
+ }[keyof Sources & string];
15
+ export type ViewEntry<Sources extends ViewSources> = {
16
+ [Name in keyof Sources & string]: {
17
+ readonly source: Name;
18
+ readonly key: SourceKey<Sources[Name]>;
19
+ readonly value: SourceValue<Sources[Name]>;
20
+ };
21
+ }[keyof Sources & string];
22
+ export interface ViewState<Sources extends ViewSources> {
23
+ readonly revision: number;
24
+ readonly sources: {
25
+ readonly [Name in keyof Sources]: SourceIdentity;
26
+ };
27
+ readonly members: readonly ViewReference<Sources>[];
28
+ }
29
+ export interface ViewBasis<Sources extends ViewSources> {
30
+ readonly viewRevision: number;
31
+ readonly sources: {
32
+ readonly [Name in keyof Sources]: SourceStamp;
33
+ };
34
+ }
35
+ export interface ViewIndex<Sources extends ViewSources> {
36
+ readonly basis: ViewBasis<Sources>;
37
+ readonly members: readonly ViewReference<Sources>[];
38
+ readonly entries: readonly ViewIndexEntry<Sources>[];
39
+ }
40
+ export interface ViewSnapshot<Sources extends ViewSources> {
41
+ readonly basis: ViewBasis<Sources>;
42
+ readonly entries: readonly ViewEntry<Sources>[];
43
+ }
44
+ export interface ViewSelection<Sources extends ViewSources> {
45
+ readonly basis: ViewBasis<Sources>;
46
+ readonly members: readonly ViewReference<Sources>[];
47
+ }
48
+ export interface ViewCommit<Sources extends ViewSources> {
49
+ readonly checkedSources: ViewSourceChecks<Sources>;
50
+ readonly previous: ViewState<Sources>;
51
+ readonly state: ViewState<Sources>;
52
+ }
53
+ export type ViewSourceChecks<Sources extends ViewSources> = {
54
+ readonly [Name in keyof Sources]?: SourceStamp;
55
+ };
56
+ export interface ViewPersistence<Sources extends ViewSources> {
57
+ commit(change: ViewCommit<Sources>): undefined;
58
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
@@ -0,0 +1,11 @@
1
+ import type { SourceIdentity, SourceStamp, ViewKey } from './source.js';
2
+ export declare function conflict(message: string): never;
3
+ export declare function data<Value>(value: Value): Value;
4
+ export declare function object(value: unknown): asserts value is Record<string, unknown>;
5
+ export declare function exactKeys(value: object, keys: readonly string[]): void;
6
+ export declare function checkIdentity(value: unknown): asserts value is SourceIdentity;
7
+ export declare function checkStamp(value: unknown): asserts value is SourceStamp;
8
+ export declare function sameIdentity(left: SourceIdentity, right: SourceIdentity): boolean;
9
+ export declare function sameStamp(left: SourceStamp, right: SourceStamp): boolean;
10
+ export declare function checkKey(key: unknown): asserts key is ViewKey;
11
+ export declare function synchronous<Value>(value: Value, label: string): Exclude<Value, PromiseLike<unknown>>;
package/dist/value.js ADDED
@@ -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{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};
package/dist/view.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { SourceBindings, SyncSourceBindings, ViewSources } from './source.js';
2
+ import type { ViewIndex, ViewPersistence, ViewReference, ViewSelection, ViewSnapshot, ViewState } from './types.js';
3
+ export declare class View<Sources extends ViewSources> {
4
+ #private;
5
+ constructor(state: ViewState<Sources>);
6
+ exportState(): ViewState<Sources>;
7
+ has(reference: ViewReference<Sources>): boolean;
8
+ members(): readonly ViewReference<Sources>[];
9
+ includeSync(sources: SyncSourceBindings<Sources>, references: readonly ViewReference<Sources>[], persistence?: ViewPersistence<Sources>): ViewState<Sources>;
10
+ excludeSync(references: readonly ViewReference<Sources>[], persistence?: ViewPersistence<Sources>): ViewState<Sources>;
11
+ index(sources: SourceBindings<Sources>): Promise<ViewIndex<Sources>>;
12
+ indexSync(sources: SyncSourceBindings<Sources>): ViewIndex<Sources>;
13
+ capture(sources: SourceBindings<Sources>, index?: ViewIndex<Sources>): Promise<ViewSnapshot<Sources>>;
14
+ captureSync(sources: SyncSourceBindings<Sources>, index?: ViewIndex<Sources>): ViewSnapshot<Sources>;
15
+ applySync(sources: SyncSourceBindings<Sources>, selection: ViewSelection<Sources>, persistence?: ViewPersistence<Sources>): ViewState<Sources>;
16
+ private beginMutation;
17
+ private install;
18
+ private current;
19
+ private indexBasis;
20
+ }
package/dist/view.js ADDED
@@ -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{PersistenceError}from"@weiliang0121/capix-diagnostics";import{basis as acceptBasis,members as acceptMembers,memberIndex,sameMembers,state as acceptState}from"./membership.js";import{ViewReading}from"./reading.js";import{conflict,data,exactKeys,object,synchronous}from"./value.js";class View{static{__name(this,"View")}#state;#members;#mutating=!1;constructor(state){this.#state=acceptState(state),this.#members=memberIndex(this.#state.members)}exportState(){return this.#state}has(reference){const[member]=acceptMembers([reference],Object.keys(this.#state.sources));return this.#members.get(member.source)?.has(member.key)??!1}members(){return this.#state.members}includeSync(sources,references,persistence){this.beginMutation();try{const previous=this.#state,added=acceptMembers(references,Object.keys(previous.sources),!0).filter(member=>!this.#members.get(member.source)?.has(member.key));if(!added.length)return previous;const checkedSources=new ViewReading(previous,sources).additionsSync(added),members=Object.freeze([...previous.members,...added]);return this.install(previous,members,checkedSources,persistence)}finally{this.#mutating=!1}}excludeSync(references,persistence){this.beginMutation();try{const previous=this.#state,requested=acceptMembers(references,Object.keys(previous.sources),!0);if(!requested.some(member=>this.#members.get(member.source)?.has(member.key)))return previous;const removed=memberIndex(requested),members=Object.freeze(previous.members.filter(member=>!removed.get(member.source)?.has(member.key)));return this.install(previous,members,Object.freeze({}),persistence)}finally{this.#mutating=!1}}async index(sources){const state=this.#state,index=await new ViewReading(state,sources).index();return this.current(state),index}indexSync(sources){const state=this.#state,index=new ViewReading(state,sources).indexSync();return this.current(state),index}async capture(sources,index){const state=this.#state,reading=new ViewReading(state,sources),basis=index===void 0?(await reading.index()).basis:this.indexBasis(index,state);this.current(state);const snapshot=await reading.capture(basis);return this.current(state),snapshot}captureSync(sources,index){const state=this.#state,reading=new ViewReading(state,sources),basis=index===void 0?reading.indexSync().basis:this.indexBasis(index,state);this.current(state);const snapshot=reading.captureSync(basis);return this.current(state),snapshot}applySync(sources,selection,persistence){this.beginMutation();try{const previous=this.#state,accepted=data(selection);object(accepted),exactKeys(accepted,["basis","members"]);const basis=acceptBasis(accepted.basis,previous),members=acceptMembers(accepted.members,Object.keys(previous.sources));return new ViewReading(previous,sources).selectionSync(members,basis),this.install(previous,members,basis.sources,persistence)}finally{this.#mutating=!1}}beginMutation(){this.#mutating&&conflict("A view mutation is already in progress"),this.#mutating=!0}install(previous,members,checkedSources,persistence){if(this.current(previous),sameMembers(previous.members,members))return previous;if(!Number.isSafeInteger(previous.revision+1))throw new RangeError("View revision is exhausted");const state=Object.freeze({revision:previous.revision+1,sources:previous.sources,members}),lookup=memberIndex(members);if(persistence)try{if(synchronous(persistence.commit(Object.freeze({checkedSources,previous,state})),"View persistence")!==void 0)throw new TypeError("View persistence must return undefined")}catch(cause){throw cause instanceof PersistenceError?cause:new PersistenceError("view.commit",cause)}return this.#state=state,this.#members=lookup,state}current(state){this.#state!==state&&conflict("View membership changed during access")}indexBasis(value,state){const index=data(value);object(index),exactKeys(index,["basis","members","entries"]);const basis=acceptBasis(index.basis,state),members=acceptMembers(index.members,Object.keys(state.sources));if(sameMembers(members,state.members)||conflict("Directory members do not match the current view"),!Array.isArray(index.entries)||!sameMembers(index.entries,state.members))throw new TypeError("Directory entries do not match its members");for(const entry of index.entries)exactKeys(entry,["source","key","summary"]);return basis}}export{View};
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@weiliang0121/capix-view",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "@weiliang0121/capix-contracts": "0.1.0",
14
+ "@weiliang0121/capix-diagnostics": "0.1.0",
15
+ "@weiliang0121/capix-resources": "0.1.0"
16
+ },
17
+ "description": "Versioned views over independently owned content for CAPiX.",
18
+ "license": "MIT",
19
+ "author": "wei.liang (https://github.com/weiliang0121)",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/weiliang0121/capix.git",
23
+ "directory": "server/view"
24
+ },
25
+ "engines": {
26
+ "node": ">=22.23.2"
27
+ },
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "files": [
31
+ "dist/**/*.js",
32
+ "dist/**/*.d.ts",
33
+ "LICENSE",
34
+ "README.md"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit"
43
+ }
6
44
  }