shared-reducer 6.3.1 → 6.3.3
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/backend/index.d.ts +9 -4
- package/backend/index.js +1 -1
- package/backend/index.mjs +1 -1
- package/package.json +1 -1
package/backend/index.d.ts
CHANGED
|
@@ -58,9 +58,10 @@ type Identifier = string | null;
|
|
|
58
58
|
type ChangeInfo<SpecT> = {
|
|
59
59
|
change: SpecT;
|
|
60
60
|
events: Readonly<Readonly<ChangeEvent>[]> | undefined;
|
|
61
|
-
error?:
|
|
61
|
+
error?: never;
|
|
62
62
|
} | {
|
|
63
|
-
change?:
|
|
63
|
+
change?: never;
|
|
64
|
+
events?: never;
|
|
64
65
|
error: string;
|
|
65
66
|
};
|
|
66
67
|
interface TopicMessage<SpecT> {
|
|
@@ -68,6 +69,7 @@ interface TopicMessage<SpecT> {
|
|
|
68
69
|
source: Identifier;
|
|
69
70
|
meta?: unknown;
|
|
70
71
|
}
|
|
72
|
+
type EventFilter = (evt: Readonly<ChangeEvent>) => boolean;
|
|
71
73
|
type ID = string;
|
|
72
74
|
declare class Broadcaster<T, SpecT> {
|
|
73
75
|
private readonly _model;
|
|
@@ -75,12 +77,14 @@ declare class Broadcaster<T, SpecT> {
|
|
|
75
77
|
private readonly _subscribers;
|
|
76
78
|
private readonly _taskQueues;
|
|
77
79
|
private readonly _idProvider;
|
|
80
|
+
private readonly _onError;
|
|
78
81
|
constructor(_model: Model<ID, T>, _context: Context<T, SpecT>, options?: {
|
|
79
82
|
subscribers?: TopicMap<ID, TopicMessage<SpecT>>;
|
|
80
83
|
taskQueues?: TaskQueueMap<ID>;
|
|
81
84
|
idProvider?: () => MaybePromise<string>;
|
|
85
|
+
onError?: (error: unknown) => void;
|
|
82
86
|
});
|
|
83
|
-
subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT
|
|
87
|
+
subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT>, eventFilter?: EventFilter | undefined): Promise<Subscription<T, SpecT, MetaT> | null>;
|
|
84
88
|
update(id: ID, change: SpecT, { events, permission, }?: {
|
|
85
89
|
events?: ChangeEvent[] | undefined;
|
|
86
90
|
permission?: Permission<T, SpecT>;
|
|
@@ -105,6 +109,7 @@ interface ServerWebSocket {
|
|
|
105
109
|
interface Access<T, SpecT> {
|
|
106
110
|
id: string;
|
|
107
111
|
permission: Permission<T, SpecT>;
|
|
112
|
+
eventFilter?: EventFilter | undefined;
|
|
108
113
|
}
|
|
109
114
|
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
|
|
110
115
|
interface WebsocketHandlerCoreOptions<AccessGetter, AcceptWebSocket> {
|
|
@@ -197,4 +202,4 @@ declare class TrackingTopicMap<K, T> implements TopicMap<K, T> {
|
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
export { AsyncTaskQueue, Broadcaster, CLOSE, CLOSE_ACK, CollectionStorageModel, InMemoryModel, InMemoryTopic, PING, PONG, PermissionError, ReadOnly, ReadWrite, ReadWriteStruct, TaskQueueMap, TrackingTopicMap, UniqueIdProvider, WebsocketHandlerFactory };
|
|
200
|
-
export type { ChangeInfo, Context, Model, Permission, Subscription, Task, TaskQueue, TaskQueueFactory, Topic, TopicMap, TopicMessage };
|
|
205
|
+
export type { ChangeEvent, ChangeInfo, Context, EventFilter, Model, Permission, Subscription, Task, TaskQueue, TaskQueueFactory, Topic, TopicMap, TopicMessage };
|
package/backend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class
|
|
1
|
+
"use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class i{u;p=new Map;constructor(t){this.u=t}async add(t,e){let r=this.p.get(t);r||(r=this.u(t),this.p.set(t,r)),await r.add(e)}async remove(t,e){const r=this.p.get(t);if(r){await r.remove(e)||this.p.delete(t)}}async broadcast(t,e){const r=this.p.get(t);r&&await r.broadcast(e)}}class n{v=new Set;add(t){this.v.add(t)}remove(t){return this.v.delete(t),this.v.size>0}broadcast(t){this.v.forEach(e=>e(t))}}const a={validateWrite(){},validateEvent(){}};class o extends Error{}class c extends Error{}const h=0,l=1,u=2,d=3,w=new Error("not found"),f=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),p=t=>t;const y="Cannot modify data",v={validateWriteSpec(){throw new o(y)},validateWrite(){throw new o(y)},validateEvent(){throw new o(y)}};exports.AsyncTaskQueue=r,exports.Broadcaster=class{m;_;v;S;O;C;constructor(t,r,a={}){this.m=t,this._=r,this.v=a.subscribers??new i(()=>new n),this.S=a.taskQueues??new s,this.O=a.idProvider??e(),this.C=a.onError??(t=>console.error(t))}async subscribe(t,e=a,r){let s={I:0},i="";const n=t=>{if(2===s.I){let e=t.message;if(r&&e.events?.length){let t;try{t=e.events.filter(r)}catch(e){this.C(`eventFilter threw: ${e}`),t=void 0}(!t||t.length<e.events.length)&&(e={...e,events:t?.length?t:void 0})}try{if(t.source===i)s.J(e,t.meta);else if(e.change){if(!e.events?.length&&this._.isNoOp?.(e.change))return;s.J(e,void 0)}}catch(t){this.C(`subscription listener threw: ${t}`)}}else 1===s.I&&s.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.m.read(t);null!=e&&(s={I:1,N:e,t:[]},await this.v.add(t,n))}),0===s.I)return null;i=await this.O()}catch(e){throw await this.v.remove(t,n),e}return{getInitialData(){if(1!==s.I)throw new Error("Already started");return s.N},listen(t){if(1!==s.I)throw new Error("Already started");const e=s.t;s={I:2,J:t},e.forEach(n)},send:(r,s,n)=>this.T(t,r,s,e,i,n),close:async()=>{await this.v.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.T(t,e,r,s,null,void 0)}async $(t,e,r,s,i,n){if(r?.length)for(const t of r)s.validateEvent(t);try{if(s.validateWriteSpec?.(e),!this._.isNoOp?.(e)){const r=await this.m.read(t);if(!r)throw new Error("Deleted");const i=this._.update(r,e),n=this.m.validate(i);s.validateWrite(n,r),await this.m.write(t,n,r)}}catch(e){return void this.v.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:i,meta:n})}0===r?.length&&(r=void 0),this.v.broadcast(t,{message:{change:e,events:r},source:i,meta:n})}async T(t,e,r,s,i,n){return this.S.push(t,()=>this.$(t,e,r,s,i,n))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{j;A;validate;D;F;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??p,this.F=s.writeErrorIntercept??p}async read(t){try{return await this.j.where(this.A,t).get()}catch(t){throw this.D(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(r,i)?r[i]:void 0)&&(s[i]?Object.defineProperty(s,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[i]=e)});try{await this.j.where(this.A,t).update(s)}catch(t){throw this.F(t)}}},exports.InMemoryModel=class{read=this.get;validate;q=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.q.set(t,e)}get(t){return this.q.get(t)}delete(t){this.q.delete(t)}write(t,e,r){if(r!==this.q.get(t))throw new Error("Unexpected previous value");this.q.set(t,e)}},exports.InMemoryTopic=n,exports.PING="P",exports.PONG="p",exports.PermissionError=o,exports.ReadOnly=v,exports.ReadWrite=a,exports.ReadWriteStruct=class{P;constructor(t=[]){this.P=t}validateWrite(t,e){for(const r of this.P){const s=Object.prototype.hasOwnProperty.call(e,r),i=Object.prototype.hasOwnProperty.call(t,r);if(s!==i)throw new o(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(i&&e[r]!==t[r])throw new o(`Cannot edit field ${String(r)}`)}}validateEvent(){}},exports.TaskQueueMap=s,exports.TrackingTopicMap=i,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:i=w,setSoftCloseHandler:n,onConnect:a,onDisconnect:p,onError:y=f}){return async(...w)=>{const f=[];let v;try{const{id:m,permission:g,eventFilter:_}=await t(...w),x=await this.broadcaster.subscribe(m,g,_);if(!x)throw i;f.push(()=>x.close());let b,E=h,S="connection failed";const O=new Promise(t=>{b=e=>{b=()=>{},S=e,t()}}),C=await e(...w);w.length=1,n?.(w[0],()=>(E===h&&(E=l,C.send("X"),v||(v=setTimeout(J,s))),O));const I=Date.now();a?.(w[0],m,g),f.push(()=>p?.(w[0],S,Date.now()-I)),C.on("close",()=>{clearTimeout(v),E=u,b("client disconnect")});const J=()=>{C.terminate(),E=d,b("connection lost")},N=()=>{C.ping(),clearTimeout(v),v=setTimeout(J,s)},T=()=>{clearTimeout(v),v=setTimeout(N,r)};C.on("pong",T),C.on("message",async(t,e)=>{if(T(),e)return C.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return C.send("p");if("x"===r)return E!==l&&E!==d?C.send(JSON.stringify({error:"Unexpected close ack message"})):(E=u,b("clean shutdown"),C.close());if(E===u)return C.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new c("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new c("Must specify change and optional id");const r={change:e.change};if("events"in e){if(!Array.isArray(e.events)||e.events.some(t=>!Array.isArray(t)||"string"!=typeof t[0]))throw new c("If specified, events must be an array of events");r.events=e.events}if("id"in e){if("number"!=typeof e.id)throw new c("If specified, id must be a number");r.id=e.id}return r}(r);await x.send(t.change,t.events,t.id)}catch(t){t instanceof o||t instanceof c?C.send(JSON.stringify({error:t.message})):(y(w[0],t,"message"),C.send(JSON.stringify({error:"Internal error"})))}}),E===h&&(C.send(JSON.stringify({init:x.getInitialData()})),x.listen((t,e)=>C.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await O}finally{clearTimeout(v);for(const t of f.reverse())try{await t()}catch(t){y(w[0],t,"teardown")}}}}};
|
package/backend/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class
|
|
1
|
+
import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class i{u;v=new Map;constructor(t){this.u=t}async add(t,e){let r=this.v.get(t);r||(r=this.u(t),this.v.set(t,r)),await r.add(e)}async remove(t,e){const r=this.v.get(t);if(r){await r.remove(e)||this.v.delete(t)}}async broadcast(t,e){const r=this.v.get(t);r&&await r.broadcast(e)}}class n{m=new Set;add(t){this.m.add(t)}remove(t){return this.m.delete(t),this.m.size>0}broadcast(t){this.m.forEach(e=>e(t))}}const a={validateWrite(){},validateEvent(){}};class o{p;_;m;S;O;C;constructor(t,r,a={}){this.p=t,this._=r,this.m=a.subscribers??new i(()=>new n),this.S=a.taskQueues??new s,this.O=a.idProvider??e(),this.C=a.onError??(t=>console.error(t))}async subscribe(t,e=a,r){let s={I:0},i="";const n=t=>{if(2===s.I){let e=t.message;if(r&&e.events?.length){let t;try{t=e.events.filter(r)}catch(e){this.C(`eventFilter threw: ${e}`),t=void 0}(!t||t.length<e.events.length)&&(e={...e,events:t?.length?t:void 0})}try{if(t.source===i)s.J(e,t.meta);else if(e.change){if(!e.events?.length&&this._.isNoOp?.(e.change))return;s.J(e,void 0)}}catch(t){this.C(`subscription listener threw: ${t}`)}}else 1===s.I&&s.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.p.read(t);null!=e&&(s={I:1,N:e,t:[]},await this.m.add(t,n))}),0===s.I)return null;i=await this.O()}catch(e){throw await this.m.remove(t,n),e}return{getInitialData(){if(1!==s.I)throw new Error("Already started");return s.N},listen(t){if(1!==s.I)throw new Error("Already started");const e=s.t;s={I:2,J:t},e.forEach(n)},send:(r,s,n)=>this.T(t,r,s,e,i,n),close:async()=>{await this.m.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.T(t,e,r,s,null,void 0)}async $(t,e,r,s,i,n){if(r?.length)for(const t of r)s.validateEvent(t);try{if(s.validateWriteSpec?.(e),!this._.isNoOp?.(e)){const r=await this.p.read(t);if(!r)throw new Error("Deleted");const i=this._.update(r,e),n=this.p.validate(i);s.validateWrite(n,r),await this.p.write(t,n,r)}}catch(e){return void this.m.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:i,meta:n})}0===r?.length&&(r=void 0),this.m.broadcast(t,{message:{change:e,events:r},source:i,meta:n})}async T(t,e,r,s,i,n){return this.S.push(t,()=>this.$(t,e,r,s,i,n))}}class c extends Error{}class h extends Error{}const l="P",u="p",d="X",w="x";class f{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:i=g,setSoftCloseHandler:n,onConnect:a,onDisconnect:o,onError:l=_}){return async(...u)=>{const d=[];let w;try{const{id:f,permission:g,eventFilter:_}=await t(...u),b=await this.broadcaster.subscribe(f,g,_);if(!b)throw i;d.push(()=>b.close());let E,S=y,O="connection failed";const C=new Promise(t=>{E=e=>{E=()=>{},O=e,t()}}),x=await e(...u);u.length=1,n?.(u[0],()=>(S===y&&(S=v,x.send("X"),w||(w=setTimeout(J,s))),C));const I=Date.now();a?.(u[0],f,g),d.push(()=>o?.(u[0],O,Date.now()-I)),x.on("close",()=>{clearTimeout(w),S=m,E("client disconnect")});const J=()=>{x.terminate(),S=p,E("connection lost")},N=()=>{x.ping(),clearTimeout(w),w=setTimeout(J,s)},T=()=>{clearTimeout(w),w=setTimeout(N,r)};x.on("pong",T),x.on("message",async(t,e)=>{if(T(),e)return x.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return x.send("p");if("x"===r)return S!==v&&S!==p?x.send(JSON.stringify({error:"Unexpected close ack message"})):(S=m,E("clean shutdown"),x.close());if(S===m)return x.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new h("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new h("Must specify change and optional id");const r={change:e.change};if("events"in e){if(!Array.isArray(e.events)||e.events.some(t=>!Array.isArray(t)||"string"!=typeof t[0]))throw new h("If specified, events must be an array of events");r.events=e.events}if("id"in e){if("number"!=typeof e.id)throw new h("If specified, id must be a number");r.id=e.id}return r}(r);await b.send(t.change,t.events,t.id)}catch(t){t instanceof c||t instanceof h?x.send(JSON.stringify({error:t.message})):(l(u[0],t,"message"),x.send(JSON.stringify({error:"Internal error"})))}}),S===y&&(x.send(JSON.stringify({init:b.getInitialData()})),b.listen((t,e)=>x.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await C}finally{clearTimeout(w);for(const t of d.reverse())try{await t()}catch(t){l(u[0],t,"teardown")}}}}}const y=0,v=1,m=2,p=3,g=new Error("not found"),_=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),b=t=>t;class E{j;A;validate;D;F;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??b,this.F=s.writeErrorIntercept??b}async read(t){try{return await this.j.where(this.A,t).get()}catch(t){throw this.D(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(r,i)?r[i]:void 0)&&(s[i]?Object.defineProperty(s,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[i]=e)});try{await this.j.where(this.A,t).update(s)}catch(t){throw this.F(t)}}}class S{read=this.get;validate;P=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.P.set(t,e)}get(t){return this.P.get(t)}delete(t){this.P.delete(t)}write(t,e,r){if(r!==this.P.get(t))throw new Error("Unexpected previous value");this.P.set(t,e)}}const O="Cannot modify data",C={validateWriteSpec(){throw new c(O)},validateWrite(){throw new c(O)},validateEvent(){throw new c(O)}};class x{W;constructor(t=[]){this.W=t}validateWrite(t,e){for(const r of this.W){const s=Object.prototype.hasOwnProperty.call(e,r),i=Object.prototype.hasOwnProperty.call(t,r);if(s!==i)throw new c(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(i&&e[r]!==t[r])throw new c(`Cannot edit field ${String(r)}`)}}validateEvent(){}}export{r as AsyncTaskQueue,o as Broadcaster,d as CLOSE,w as CLOSE_ACK,E as CollectionStorageModel,S as InMemoryModel,n as InMemoryTopic,l as PING,u as PONG,c as PermissionError,C as ReadOnly,a as ReadWrite,x as ReadWriteStruct,s as TaskQueueMap,i as TrackingTopicMap,e as UniqueIdProvider,f as WebsocketHandlerFactory};
|