shared-reducer 6.3.0 → 6.3.2
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 +7 -4
- package/backend/index.js +1 -1
- package/backend/index.mjs +1 -1
- package/frontend/index.d.ts +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;
|
|
@@ -80,7 +82,7 @@ declare class Broadcaster<T, SpecT> {
|
|
|
80
82
|
taskQueues?: TaskQueueMap<ID>;
|
|
81
83
|
idProvider?: () => MaybePromise<string>;
|
|
82
84
|
});
|
|
83
|
-
subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT
|
|
85
|
+
subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT>, eventFilter?: EventFilter): Promise<Subscription<T, SpecT, MetaT> | null>;
|
|
84
86
|
update(id: ID, change: SpecT, { events, permission, }?: {
|
|
85
87
|
events?: ChangeEvent[] | undefined;
|
|
86
88
|
permission?: Permission<T, SpecT>;
|
|
@@ -105,6 +107,7 @@ interface ServerWebSocket {
|
|
|
105
107
|
interface Access<T, SpecT> {
|
|
106
108
|
id: string;
|
|
107
109
|
permission: Permission<T, SpecT>;
|
|
110
|
+
eventFilter?: EventFilter;
|
|
108
111
|
}
|
|
109
112
|
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
|
|
110
113
|
interface WebsocketHandlerCoreOptions<AccessGetter, AcceptWebSocket> {
|
|
@@ -197,4 +200,4 @@ declare class TrackingTopicMap<K, T> implements TopicMap<K, T> {
|
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
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 };
|
|
203
|
+
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;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()}async subscribe(t,e=a,r){let s={C:0},i="";const n=t=>{if(2===s.C){let e=t.message;if(r&&e.events?.length){const t=e.events.filter(r);e={...e,events:t.length?t:void 0}}if(t.source===i)s.I(e,t.meta);else if(e.change){if(!e.events?.length&&this._.isNoOp?.(e.change))return;s.I(e,void 0)}}else 1===s.C&&s.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.m.read(t);null!=e&&(s={C:1,J:e,t:[]},await this.v.add(t,n))}),0===s.C)return null;i=await this.O()}catch(e){throw await this.v.remove(t,n),e}return{getInitialData(){if(1!==s.C)throw new Error("Already started");return s.J},listen(t){if(1!==s.C)throw new Error("Already started");const e=s.t;s={C:2,I:t},e.forEach(n)},send:(r,s,n)=>this.N(t,r,s,e,i,n),close:async()=>{await this.v.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.N(t,e,r,s,null,void 0)}async T(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 N(t,e,r,s,i,n){return this.S.push(t,()=>this.T(t,e,r,s,i,n))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{j;A;validate;D;$;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??p,this.$=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.$(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{F;constructor(t=[]){this.F=t}validateWrite(t,e){for(const r of this.F){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;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()}async subscribe(t,e=a,r){let s={C:0},i="";const n=t=>{if(2===s.C){let e=t.message;if(r&&e.events?.length){const t=e.events.filter(r);e={...e,events:t.length?t:void 0}}if(t.source===i)s.I(e,t.meta);else if(e.change){if(!e.events?.length&&this._.isNoOp?.(e.change))return;s.I(e,void 0)}}else 1===s.C&&s.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.p.read(t);null!=e&&(s={C:1,J:e,t:[]},await this.m.add(t,n))}),0===s.C)return null;i=await this.O()}catch(e){throw await this.m.remove(t,n),e}return{getInitialData(){if(1!==s.C)throw new Error("Already started");return s.J},listen(t){if(1!==s.C)throw new Error("Already started");const e=s.t;s={C:2,I:t},e.forEach(n)},send:(r,s,n)=>this.N(t,r,s,e,i,n),close:async()=>{await this.m.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.N(t,e,r,s,null,void 0)}async T(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 N(t,e,r,s,i,n){return this.S.push(t,()=>this.T(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;$;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??b,this.$=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.$(t)}}}class S{read=this.get;validate;F=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.F.set(t,e)}get(t){return this.F.get(t)}delete(t){this.F.delete(t)}write(t,e,r){if(r!==this.F.get(t))throw new Error("Unexpected previous value");this.F.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{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 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};
|
package/frontend/index.d.ts
CHANGED
|
@@ -119,4 +119,4 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export { AT_LEAST_ONCE, AT_MOST_ONCE, OnlineScheduler, SharedReducer, exponentialDelay };
|
|
122
|
-
export type { ChangeEvent, ConnectionInfo, Context, DeliveryStrategy, DisconnectDetail, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
|
|
122
|
+
export type { ChangeEvent, ConnectionInfo, Context, DeliveryStrategy, DisconnectDetail, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions, StateListener };
|