@arcote.tech/arc 0.3.6 → 0.4.1

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.
Files changed (40) hide show
  1. package/dist/adapters/auth-adapter.d.ts +35 -20
  2. package/dist/adapters/command-wire.d.ts +4 -2
  3. package/dist/adapters/event-wire.d.ts +28 -3
  4. package/dist/adapters/index.d.ts +1 -0
  5. package/dist/adapters/query-wire.d.ts +6 -4
  6. package/dist/adapters/wire.d.ts +7 -11
  7. package/dist/context-element/aggregate/aggregate-base.d.ts +31 -0
  8. package/dist/context-element/aggregate/aggregate-builder.d.ts +139 -0
  9. package/dist/context-element/aggregate/aggregate-data.d.ts +114 -0
  10. package/dist/context-element/aggregate/aggregate-element.d.ts +69 -0
  11. package/dist/context-element/aggregate/index.d.ts +31 -0
  12. package/dist/context-element/aggregate/simple-aggregate.d.ts +94 -0
  13. package/dist/context-element/command/command-context.d.ts +3 -32
  14. package/dist/context-element/context-element.d.ts +7 -1
  15. package/dist/context-element/element-context.d.ts +39 -0
  16. package/dist/context-element/index.d.ts +2 -0
  17. package/dist/context-element/route/route.d.ts +8 -26
  18. package/dist/context-element/static-view/static-view.d.ts +2 -2
  19. package/dist/context-element/view/index.d.ts +1 -1
  20. package/dist/data-storage/store-state.abstract.d.ts +1 -0
  21. package/dist/elements/branded.d.ts +1 -0
  22. package/dist/elements/object.d.ts +2 -2
  23. package/dist/elements/optional.d.ts +1 -1
  24. package/dist/fragment/arc-fragment.d.ts +18 -0
  25. package/dist/fragment/index.d.ts +2 -0
  26. package/dist/index.d.ts +16 -0
  27. package/dist/index.js +1006 -458
  28. package/dist/model/context-accessor.d.ts +51 -0
  29. package/dist/model/index.d.ts +4 -0
  30. package/dist/model/live-query/live-query.d.ts +4 -26
  31. package/dist/model/model-adapters.d.ts +5 -0
  32. package/dist/model/model-like.d.ts +9 -0
  33. package/dist/model/model.d.ts +10 -1
  34. package/dist/model/mutation-executor/mutation-executor.d.ts +2 -2
  35. package/dist/model/scoped-model.d.ts +66 -0
  36. package/dist/model/scoped-wire-proxy.d.ts +23 -0
  37. package/dist/streaming/index.d.ts +0 -2
  38. package/dist/streaming/streaming-live-query.d.ts +10 -12
  39. package/dist/streaming/streaming-query-cache.d.ts +25 -4
  40. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,71 +1,126 @@
1
1
  // src/adapters/auth-adapter.ts
2
+ var TOKEN_PREFIX = "arc:token:";
3
+ function hasLocalStorage() {
4
+ return typeof localStorage !== "undefined";
5
+ }
6
+
2
7
  class AuthAdapter {
3
- token = null;
4
- decoded = null;
5
- setToken(token) {
6
- this.token = token;
8
+ scopes = new Map;
9
+ setToken(token, scope = "default") {
7
10
  if (!token) {
8
- this.decoded = null;
11
+ this.scopes.delete(scope);
12
+ if (hasLocalStorage())
13
+ localStorage.removeItem(TOKEN_PREFIX + scope);
9
14
  return;
10
15
  }
11
16
  try {
12
17
  const parts = token.split(".");
13
18
  if (parts.length !== 3) {
14
- this.decoded = null;
19
+ this.scopes.delete(scope);
20
+ if (hasLocalStorage())
21
+ localStorage.removeItem(TOKEN_PREFIX + scope);
15
22
  return;
16
23
  }
17
24
  const payload = JSON.parse(atob(parts[1]));
18
- this.decoded = {
19
- tokenName: payload.tokenName,
20
- params: payload.params || {},
21
- iat: payload.iat,
22
- exp: payload.exp
23
- };
25
+ this.scopes.set(scope, {
26
+ raw: token,
27
+ decoded: {
28
+ tokenName: payload.tokenName,
29
+ params: payload.params || {},
30
+ iat: payload.iat,
31
+ exp: payload.exp
32
+ }
33
+ });
34
+ if (hasLocalStorage())
35
+ localStorage.setItem(TOKEN_PREFIX + scope, token);
24
36
  } catch {
25
- this.decoded = null;
37
+ this.scopes.delete(scope);
38
+ if (hasLocalStorage())
39
+ localStorage.removeItem(TOKEN_PREFIX + scope);
26
40
  }
27
41
  }
28
- getToken() {
29
- return this.token;
42
+ loadPersisted() {
43
+ if (!hasLocalStorage())
44
+ return;
45
+ for (let i = 0;i < localStorage.length; i++) {
46
+ const key = localStorage.key(i);
47
+ if (key?.startsWith(TOKEN_PREFIX)) {
48
+ const scope = key.slice(TOKEN_PREFIX.length);
49
+ const raw = localStorage.getItem(key);
50
+ if (raw) {
51
+ try {
52
+ const parts = raw.split(".");
53
+ if (parts.length !== 3)
54
+ continue;
55
+ const payload = JSON.parse(atob(parts[1]));
56
+ this.scopes.set(scope, {
57
+ raw,
58
+ decoded: {
59
+ tokenName: payload.tokenName,
60
+ params: payload.params || {},
61
+ iat: payload.iat,
62
+ exp: payload.exp
63
+ }
64
+ });
65
+ } catch {
66
+ localStorage.removeItem(key);
67
+ }
68
+ }
69
+ }
70
+ }
30
71
  }
31
- getDecoded() {
32
- return this.decoded;
72
+ getToken(scope = "default") {
73
+ return this.scopes.get(scope)?.raw ?? null;
33
74
  }
34
- getParams() {
35
- return this.decoded?.params ?? null;
75
+ getDecoded(scope = "default") {
76
+ return this.scopes.get(scope)?.decoded ?? null;
36
77
  }
37
- getTokenName() {
38
- return this.decoded?.tokenName ?? null;
78
+ getParams(scope = "default") {
79
+ return this.scopes.get(scope)?.decoded?.params ?? null;
39
80
  }
40
- isAuthenticated() {
41
- return this.decoded !== null;
81
+ getTokenName(scope = "default") {
82
+ return this.scopes.get(scope)?.decoded?.tokenName ?? null;
42
83
  }
43
- isExpired() {
44
- if (!this.decoded?.exp)
84
+ isAuthenticated(scope) {
85
+ if (scope !== undefined) {
86
+ return this.scopes.has(scope);
87
+ }
88
+ return this.scopes.size > 0;
89
+ }
90
+ isExpired(scope = "default") {
91
+ const entry = this.scopes.get(scope);
92
+ if (!entry?.decoded?.exp)
45
93
  return false;
46
- return Date.now() > this.decoded.exp;
94
+ return Date.now() > entry.decoded.exp;
95
+ }
96
+ getAllScopes() {
97
+ return Array.from(this.scopes.keys());
98
+ }
99
+ clear() {
100
+ if (hasLocalStorage()) {
101
+ for (const scope of this.scopes.keys()) {
102
+ localStorage.removeItem(TOKEN_PREFIX + scope);
103
+ }
104
+ }
105
+ this.scopes.clear();
47
106
  }
48
107
  }
49
108
  // src/adapters/wire.ts
50
109
  class Wire {
51
- token = null;
52
110
  baseUrl;
53
111
  constructor(baseUrl) {
54
112
  this.baseUrl = baseUrl;
55
113
  }
56
- setAuthToken(token) {
57
- this.token = token;
58
- }
59
- getAuthToken() {
60
- return this.token;
61
- }
62
114
  getBaseUrl() {
63
115
  return this.baseUrl;
64
116
  }
65
- async fetch(endpoint, options = {}) {
117
+ async fetch(endpoint, options = {}, auth) {
66
118
  const headers = new Headers(options.headers);
67
- if (this.token) {
68
- headers.set("Authorization", `Bearer ${this.token}`);
119
+ if (auth?.token) {
120
+ headers.set("Authorization", `Bearer ${auth.token}`);
121
+ }
122
+ if (auth?.scope) {
123
+ headers.set("X-Arc-Scope", auth.scope);
69
124
  }
70
125
  if (options.body && !headers.has("Content-Type") && typeof options.body === "string") {
71
126
  headers.set("Content-Type", "application/json");
@@ -83,7 +138,7 @@ class CommandWire extends Wire {
83
138
  constructor(baseUrl) {
84
139
  super(baseUrl);
85
140
  }
86
- async executeCommand(name, params) {
141
+ async executeCommand(name, params, auth) {
87
142
  const hasFile = this.containsFile(params);
88
143
  let body;
89
144
  let headers;
@@ -97,7 +152,7 @@ class CommandWire extends Wire {
97
152
  method: "POST",
98
153
  body,
99
154
  headers
100
- });
155
+ }, auth);
101
156
  if (!response.ok) {
102
157
  const errorText = await response.text();
103
158
  throw new Error(`Command ${name} failed: ${response.status} ${errorText}`);
@@ -135,24 +190,39 @@ class EventWire {
135
190
  instanceId;
136
191
  ws = null;
137
192
  state = "disconnected";
138
- token = null;
193
+ scopeTokens = new Map;
139
194
  lastHostEventId = null;
140
195
  pendingEvents = [];
141
196
  onEventCallback;
142
197
  onSyncedCallback;
143
198
  reconnectTimeout;
144
199
  syncRequested = false;
200
+ viewSubscriptions = new Map;
201
+ viewSubCounter = 0;
202
+ pendingViewSubs = [];
145
203
  constructor(baseUrl) {
146
204
  this.baseUrl = baseUrl;
147
205
  this.instanceId = ++eventWireInstanceCounter;
148
206
  }
149
- setAuthToken(token) {
150
- this.token = token;
151
- if (this.state === "connected") {
152
- this.disconnect();
153
- this.connect();
207
+ setScopeToken(scope, token) {
208
+ if (token === null) {
209
+ this.scopeTokens.delete(scope);
210
+ } else {
211
+ this.scopeTokens.set(scope, token);
212
+ }
213
+ if (this.state === "connected" && this.ws) {
214
+ if (token !== null) {
215
+ this.ws.send(JSON.stringify({
216
+ type: "scope:auth",
217
+ scope,
218
+ token
219
+ }));
220
+ }
154
221
  }
155
222
  }
223
+ hasScopeTokens() {
224
+ return this.scopeTokens.size > 0;
225
+ }
156
226
  setLastHostEventId(id) {
157
227
  this.lastHostEventId = id;
158
228
  }
@@ -171,15 +241,16 @@ class EventWire {
171
241
  const host = typeof window !== "undefined" ? window.location.host : "localhost";
172
242
  wsUrl = `${protocol}//${host}${this.baseUrl}`;
173
243
  }
174
- const tokenParam = this.token ? `?token=${this.token}` : "";
175
- const url = `${wsUrl}/ws${tokenParam}`;
244
+ const url = `${wsUrl}/ws`;
176
245
  try {
177
246
  this.ws = new WebSocket(url);
178
247
  this.ws.onopen = () => {
179
248
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
180
249
  this.state = "connected";
250
+ this.sendAllScopeTokens();
181
251
  this.requestSync();
182
252
  this.flushPendingEvents();
253
+ this.flushPendingViewSubs();
183
254
  } else {
184
255
  console.log(`[EventWire] onopen called but ws is not OPEN, readyState:`, this.ws?.readyState);
185
256
  }
@@ -254,9 +325,45 @@ class EventWire {
254
325
  onSynced(callback) {
255
326
  this.onSyncedCallback = callback;
256
327
  }
328
+ subscribeQuery(descriptor, callback, scope) {
329
+ const subscriptionId = `qs_${++this.viewSubCounter}_${Date.now()}`;
330
+ this.viewSubscriptions.set(subscriptionId, callback);
331
+ if (this.state === "connected" && this.ws) {
332
+ this.ws.send(JSON.stringify({
333
+ type: "subscribe-query",
334
+ subscriptionId,
335
+ descriptor,
336
+ scope
337
+ }));
338
+ } else {
339
+ this.pendingViewSubs.push({ subscriptionId, descriptor, scope });
340
+ }
341
+ return subscriptionId;
342
+ }
343
+ unsubscribeQuery(subscriptionId) {
344
+ this.viewSubscriptions.delete(subscriptionId);
345
+ if (this.state === "connected" && this.ws) {
346
+ this.ws.send(JSON.stringify({
347
+ type: "unsubscribe-query",
348
+ subscriptionId
349
+ }));
350
+ }
351
+ this.pendingViewSubs = this.pendingViewSubs.filter((s) => s.subscriptionId !== subscriptionId);
352
+ }
257
353
  getState() {
258
354
  return this.state;
259
355
  }
356
+ sendAllScopeTokens() {
357
+ if (!this.ws || this.state !== "connected")
358
+ return;
359
+ for (const [scope, token] of this.scopeTokens) {
360
+ this.ws.send(JSON.stringify({
361
+ type: "scope:auth",
362
+ scope,
363
+ token
364
+ }));
365
+ }
366
+ }
260
367
  handleMessage(message) {
261
368
  switch (message.type) {
262
369
  case "events":
@@ -274,6 +381,13 @@ class EventWire {
274
381
  this.lastHostEventId = message.lastHostEventId;
275
382
  }
276
383
  break;
384
+ case "query-data": {
385
+ const cb = this.viewSubscriptions.get(message.subscriptionId);
386
+ if (cb) {
387
+ cb(message.data);
388
+ }
389
+ break;
390
+ }
277
391
  case "command-result":
278
392
  break;
279
393
  case "error":
@@ -298,6 +412,19 @@ class EventWire {
298
412
  this.pendingEvents = [];
299
413
  }
300
414
  }
415
+ flushPendingViewSubs() {
416
+ if (!this.ws || this.state !== "connected")
417
+ return;
418
+ for (const sub of this.pendingViewSubs) {
419
+ this.ws.send(JSON.stringify({
420
+ type: "subscribe-query",
421
+ subscriptionId: sub.subscriptionId,
422
+ descriptor: sub.descriptor,
423
+ scope: sub.scope
424
+ }));
425
+ }
426
+ this.pendingViewSubs = [];
427
+ }
301
428
  scheduleReconnect() {
302
429
  if (this.reconnectTimeout)
303
430
  return;
@@ -312,18 +439,18 @@ class QueryWire extends Wire {
312
439
  constructor(baseUrl) {
313
440
  super(baseUrl);
314
441
  }
315
- async query(viewName, options = {}) {
442
+ async query(viewName, options = {}, auth) {
316
443
  const response = await this.fetch(`/query/${viewName}`, {
317
444
  method: "POST",
318
445
  body: JSON.stringify(options)
319
- });
446
+ }, auth);
320
447
  if (!response.ok) {
321
448
  const errorText = await response.text();
322
449
  throw new Error(`Query ${viewName} failed: ${response.status} ${errorText}`);
323
450
  }
324
451
  return await response.json();
325
452
  }
326
- stream(viewName, options, callback, token) {
453
+ stream(viewName, options, callback, auth) {
327
454
  const params = new URLSearchParams;
328
455
  if (options?.where) {
329
456
  params.set("where", JSON.stringify(options.where));
@@ -334,9 +461,11 @@ class QueryWire extends Wire {
334
461
  if (options?.limit) {
335
462
  params.set("limit", String(options.limit));
336
463
  }
337
- const authToken = token ?? this.getAuthToken();
338
- if (authToken) {
339
- params.set("token", authToken);
464
+ if (auth?.token) {
465
+ params.set("token", auth.token);
466
+ }
467
+ if (auth?.scope) {
468
+ params.set("scope", auth.scope);
340
469
  }
341
470
  const queryString = params.toString();
342
471
  const url = `${this.getBaseUrl()}/stream/${viewName}${queryString ? `?${queryString}` : ""}`;
@@ -453,6 +582,18 @@ class LocalEventPublisher {
453
582
  }
454
583
  }
455
584
  async notifySubscribers(event) {
585
+ const wildcardSubs = this.subscribers.get("*");
586
+ if (wildcardSubs) {
587
+ for (const callback of wildcardSubs) {
588
+ try {
589
+ const result = callback(event);
590
+ if (result instanceof Promise)
591
+ await result;
592
+ } catch (error) {
593
+ console.error(`[EventPublisher] Wildcard subscriber error:`, error);
594
+ }
595
+ }
596
+ }
456
597
  const subs = this.subscribers.get(event.type);
457
598
  if (!subs || subs.size === 0)
458
599
  return;
@@ -598,8 +739,8 @@ class ArcOptional {
598
739
  return null;
599
740
  }
600
741
  deserialize(value) {
601
- if (!value)
602
- return null;
742
+ if (value == null)
743
+ return;
603
744
  return this.parent.deserialize(value);
604
745
  }
605
746
  validate(value) {
@@ -636,6 +777,9 @@ class ArcBranded {
636
777
  this.parent = parent;
637
778
  this.brand = brand;
638
779
  }
780
+ get name() {
781
+ return this.brand;
782
+ }
639
783
  serialize(value) {
640
784
  return this.parent.serialize(value);
641
785
  }
@@ -1109,187 +1253,6 @@ function object(element) {
1109
1253
  return new ArcObject(element);
1110
1254
  }
1111
1255
 
1112
- // src/context-element/context-element.ts
1113
- class ArcContextElement {
1114
- name;
1115
- constructor(name) {
1116
- this.name = name;
1117
- }
1118
- }
1119
-
1120
- // src/context-element/command/command.ts
1121
- class ArcCommand extends ArcContextElement {
1122
- data;
1123
- constructor(data) {
1124
- super(data.name);
1125
- this.data = data;
1126
- }
1127
- description(description) {
1128
- return new ArcCommand({
1129
- ...this.data,
1130
- description
1131
- });
1132
- }
1133
- get isPublic() {
1134
- return !this.hasProtections;
1135
- }
1136
- query(elements) {
1137
- return new ArcCommand({
1138
- ...this.data,
1139
- queryElements: elements
1140
- });
1141
- }
1142
- mutate(elements) {
1143
- return new ArcCommand({
1144
- ...this.data,
1145
- mutationElements: elements
1146
- });
1147
- }
1148
- withParams(schema) {
1149
- return new ArcCommand({
1150
- ...this.data,
1151
- params: schema instanceof ArcObject ? schema : new ArcObject(schema)
1152
- });
1153
- }
1154
- withResult(...schemas) {
1155
- return new ArcCommand({
1156
- ...this.data,
1157
- results: schemas
1158
- });
1159
- }
1160
- handle(handler) {
1161
- return new ArcCommand({
1162
- ...this.data,
1163
- handler
1164
- });
1165
- }
1166
- protectBy(token, check) {
1167
- const existingProtections = this.data.protections || [];
1168
- return new ArcCommand({
1169
- ...this.data,
1170
- protections: [...existingProtections, { token, check }]
1171
- });
1172
- }
1173
- get hasProtections() {
1174
- return (this.data.protections?.length ?? 0) > 0;
1175
- }
1176
- get protections() {
1177
- return this.data.protections || [];
1178
- }
1179
- async verifyProtections(tokens) {
1180
- if (!this.data.protections || this.data.protections.length === 0) {
1181
- return true;
1182
- }
1183
- for (const protection of this.data.protections) {
1184
- const tokenInstance = tokens.find((t) => t.getTokenDefinition() === protection.token);
1185
- if (!tokenInstance) {
1186
- return false;
1187
- }
1188
- const allowed = await protection.check(tokenInstance);
1189
- if (!allowed) {
1190
- return false;
1191
- }
1192
- }
1193
- return true;
1194
- }
1195
- async init(environment, adapters) {
1196
- if (environment === "server" && this.data.handler && adapters.commandWire?.registerCommandHandler) {
1197
- adapters.commandWire.registerCommandHandler(this.data.name, async (params) => {
1198
- const executeFunc = this.mutateContext(adapters);
1199
- return await executeFunc(params);
1200
- });
1201
- }
1202
- }
1203
- mutateContext(adapters) {
1204
- const executeFunc = async (params) => {
1205
- if (this.data.handler) {
1206
- return await this.executeLocally(params, adapters);
1207
- }
1208
- if (!adapters.commandWire) {
1209
- throw new Error(`Command "${this.data.name}" has no handler and no commandWire adapter available for remote execution`);
1210
- }
1211
- return await adapters.commandWire.executeCommand(this.data.name, params);
1212
- };
1213
- return Object.assign(executeFunc, { params: this.data.params });
1214
- }
1215
- async executeLocally(params, adapters) {
1216
- if (!this.data.handler) {
1217
- throw new Error(`Command "${this.data.name}" has no handler`);
1218
- }
1219
- const context2 = this.buildCommandContext(adapters);
1220
- const result = await this.data.handler(context2, params);
1221
- return result;
1222
- }
1223
- buildCommandContext(adapters) {
1224
- const context2 = {};
1225
- const elementMap = new Map;
1226
- for (const element of this.data.queryElements) {
1227
- if (element.queryContext) {
1228
- const elementContext = element.queryContext(adapters);
1229
- context2[element.name] = elementContext;
1230
- elementMap.set(element, elementContext);
1231
- }
1232
- }
1233
- for (const element of this.data.mutationElements) {
1234
- if (element.mutateContext) {
1235
- const elementContext = element.mutateContext(adapters);
1236
- context2[element.name] = elementContext;
1237
- elementMap.set(element, elementContext);
1238
- }
1239
- }
1240
- context2.get = (element) => {
1241
- const cached = elementMap.get(element);
1242
- if (cached)
1243
- return cached;
1244
- if (element.queryContext) {
1245
- const ctx = element.queryContext(adapters);
1246
- elementMap.set(element, ctx);
1247
- return ctx;
1248
- }
1249
- if (element.mutateContext) {
1250
- const ctx = element.mutateContext(adapters);
1251
- elementMap.set(element, ctx);
1252
- return ctx;
1253
- }
1254
- throw new Error(`Element "${element.name}" has no context available`);
1255
- };
1256
- if (this.hasProtections && adapters.authAdapter) {
1257
- const decoded = adapters.authAdapter.getDecoded();
1258
- if (decoded) {
1259
- context2.$auth = {
1260
- params: decoded.params,
1261
- tokenName: decoded.tokenName
1262
- };
1263
- } else {
1264
- throw new Error(`Command "${this.data.name}" requires authentication but no valid token found`);
1265
- }
1266
- }
1267
- return context2;
1268
- }
1269
- toJsonSchema() {
1270
- const parametersSchema = this.data.params ? this.data.params.toJsonSchema?.() ?? {
1271
- type: "object",
1272
- properties: {}
1273
- } : { type: "object", properties: {} };
1274
- return {
1275
- type: "function",
1276
- name: this.data.name,
1277
- description: this.data.description ?? undefined,
1278
- parameters: parametersSchema,
1279
- strict: true
1280
- };
1281
- }
1282
- }
1283
- function command(name) {
1284
- return new ArcCommand({
1285
- name,
1286
- params: null,
1287
- results: [],
1288
- queryElements: [],
1289
- mutationElements: [],
1290
- protections: []
1291
- });
1292
- }
1293
1256
  // src/elements/abstract-primitive.ts
1294
1257
  class ArcPrimitive extends ArcAbstract {
1295
1258
  serialize(value) {
@@ -1582,8 +1545,28 @@ function number() {
1582
1545
  return new ArcNumber;
1583
1546
  }
1584
1547
 
1585
- // src/context-element/event/event.ts
1586
- class ArcEvent extends ArcContextElement {
1548
+ // src/fragment/arc-fragment.ts
1549
+ class ArcFragmentBase {
1550
+ is(type) {
1551
+ return this.types.includes(type);
1552
+ }
1553
+ }
1554
+
1555
+ // src/context-element/context-element.ts
1556
+ class ArcContextElement extends ArcFragmentBase {
1557
+ name;
1558
+ types = ["context-element"];
1559
+ get id() {
1560
+ return this.name;
1561
+ }
1562
+ constructor(name) {
1563
+ super();
1564
+ this.name = name;
1565
+ }
1566
+ }
1567
+
1568
+ // src/context-element/event/event.ts
1569
+ class ArcEvent extends ArcContextElement {
1587
1570
  data;
1588
1571
  eventId = id("event");
1589
1572
  constructor(data) {
@@ -1615,11 +1598,523 @@ class ArcEvent extends ArcContextElement {
1615
1598
  }
1616
1599
  };
1617
1600
  }
1618
- protectBy(token, protectionFn) {
1601
+ protectBy(token, protectionFn) {
1602
+ const existingProtections = this.data.protections || [];
1603
+ return new ArcEvent({
1604
+ ...this.data,
1605
+ protections: [...existingProtections, { token, protectionFn }]
1606
+ });
1607
+ }
1608
+ get hasProtections() {
1609
+ return (this.data.protections?.length ?? 0) > 0;
1610
+ }
1611
+ get protections() {
1612
+ return this.data.protections || [];
1613
+ }
1614
+ getProtectionFor(tokenInstance) {
1615
+ if (!this.data.protections) {
1616
+ return null;
1617
+ }
1618
+ for (const protection of this.data.protections) {
1619
+ if (tokenInstance.getTokenDefinition() === protection.token) {
1620
+ return protection.protectionFn(tokenInstance.params);
1621
+ }
1622
+ }
1623
+ return null;
1624
+ }
1625
+ tags(tagFields) {
1626
+ return new ArcEvent({
1627
+ ...this.data,
1628
+ tagFields
1629
+ });
1630
+ }
1631
+ get tagFields() {
1632
+ return this.data.tagFields || [];
1633
+ }
1634
+ static _sharedSchema = null;
1635
+ static sharedDatabaseStoreSchema() {
1636
+ if (ArcEvent._sharedSchema) {
1637
+ return ArcEvent._sharedSchema;
1638
+ }
1639
+ const eventsSchema = new ArcObject({
1640
+ _id: new ArcString().primaryKey(),
1641
+ type: new ArcString().index(),
1642
+ payload: new ArcString,
1643
+ createdAt: new ArcString().index()
1644
+ });
1645
+ const tagsSchema = new ArcObject({
1646
+ _id: new ArcString().primaryKey(),
1647
+ eventId: new ArcString().index(),
1648
+ tagKey: new ArcString().index(),
1649
+ tagValue: new ArcString().index()
1650
+ });
1651
+ const syncStatusSchema = new ArcObject({
1652
+ _id: new ArcString().primaryKey(),
1653
+ eventId: new ArcString().index(),
1654
+ synced: new ArcBoolean().index(),
1655
+ timestamp: new ArcNumber,
1656
+ retryCount: new ArcNumber
1657
+ });
1658
+ ArcEvent._sharedSchema = {
1659
+ tables: [
1660
+ { name: "events", schema: eventsSchema },
1661
+ { name: "event_tags", schema: tagsSchema },
1662
+ { name: "event_sync_status", schema: syncStatusSchema }
1663
+ ]
1664
+ };
1665
+ return ArcEvent._sharedSchema;
1666
+ }
1667
+ databaseStoreSchema() {
1668
+ return ArcEvent.sharedDatabaseStoreSchema();
1669
+ }
1670
+ }
1671
+ function event(name, payload) {
1672
+ return new ArcEvent({
1673
+ name,
1674
+ payload: payload ? payload instanceof ArcObject ? payload : new ArcObject(payload) : undefined,
1675
+ protections: []
1676
+ });
1677
+ }
1678
+
1679
+ // src/context-element/aggregate/aggregate-base.ts
1680
+ class AggregateBase {
1681
+ value;
1682
+ _id;
1683
+ #adapters;
1684
+ constructor(row, adapters) {
1685
+ this._id = row._id;
1686
+ const { _id, ...rest } = row;
1687
+ this.value = rest;
1688
+ this.#adapters = adapters;
1689
+ }
1690
+ toJSON() {
1691
+ return { _id: this._id, ...this.value };
1692
+ }
1693
+ get ctx() {
1694
+ const events = this.constructor.__aggregateEvents ?? [];
1695
+ const adapters = this.#adapters;
1696
+ const result = {};
1697
+ for (const entry of events) {
1698
+ const arcEvent = entry.event;
1699
+ result[arcEvent.name] = {
1700
+ emit: async (payload) => {
1701
+ if (!adapters.eventPublisher) {
1702
+ throw new Error(`Cannot emit event "${arcEvent.name}": no eventPublisher adapter available`);
1703
+ }
1704
+ const eventInstance = {
1705
+ type: arcEvent.name,
1706
+ payload,
1707
+ id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
1708
+ createdAt: new Date
1709
+ };
1710
+ await adapters.eventPublisher.publish(eventInstance);
1711
+ }
1712
+ };
1713
+ }
1714
+ return result;
1715
+ }
1716
+ }
1717
+
1718
+ // src/context-element/aggregate/aggregate-builder.ts
1719
+ function createInlineEvent(eventName, payload) {
1720
+ const po = payload instanceof ArcObject ? payload : new ArcObject(payload);
1721
+ return new ArcEvent({
1722
+ name: eventName,
1723
+ payload: po,
1724
+ protections: []
1725
+ });
1726
+ }
1727
+
1728
+ class AggregateBuilder {
1729
+ _name;
1730
+ _id;
1731
+ _schema;
1732
+ _events;
1733
+ _protections;
1734
+ _mutateMethods;
1735
+ _queryMethods;
1736
+ constructor(name, id2, schema, events = [], protections = [], mutateMethods = [], queryMethods = []) {
1737
+ this._name = name;
1738
+ this._id = id2;
1739
+ this._schema = schema;
1740
+ this._events = events;
1741
+ this._protections = protections;
1742
+ this._mutateMethods = mutateMethods;
1743
+ this._queryMethods = queryMethods;
1744
+ }
1745
+ protectBy(token, protectionFn) {
1746
+ return new AggregateBuilder(this._name, this._id, this._schema, this._events, [...this._protections, { token, protectionFn }], this._mutateMethods, this._queryMethods);
1747
+ }
1748
+ event(eventName, payload, handler) {
1749
+ const arcEvent = createInlineEvent(eventName, payload);
1750
+ const entry = {
1751
+ event: arcEvent,
1752
+ handler,
1753
+ isInline: true,
1754
+ isPublic: false
1755
+ };
1756
+ return new AggregateBuilder(this._name, this._id, this._schema, [...this._events, entry], this._protections, this._mutateMethods, this._queryMethods);
1757
+ }
1758
+ publicEvent(eventName, payload, handler) {
1759
+ const arcEvent = createInlineEvent(eventName, payload);
1760
+ const entry = {
1761
+ event: arcEvent,
1762
+ handler,
1763
+ isInline: true,
1764
+ isPublic: true
1765
+ };
1766
+ return new AggregateBuilder(this._name, this._id, this._schema, [...this._events, entry], this._protections, this._mutateMethods, this._queryMethods);
1767
+ }
1768
+ handleEvent(arcEvent, handler) {
1769
+ const entry = {
1770
+ event: arcEvent,
1771
+ handler,
1772
+ isInline: false,
1773
+ isPublic: false
1774
+ };
1775
+ return new AggregateBuilder(this._name, this._id, this._schema, [...this._events, entry], this._protections, this._mutateMethods, this._queryMethods);
1776
+ }
1777
+ mutateMethod(methodName, config, handler) {
1778
+ const paramsObj = config.params instanceof ArcObject ? config.params : new ArcObject(config.params);
1779
+ const entry = {
1780
+ name: methodName,
1781
+ params: paramsObj,
1782
+ handler
1783
+ };
1784
+ return new AggregateBuilder(this._name, this._id, this._schema, this._events, this._protections, [...this._mutateMethods, entry], this._queryMethods);
1785
+ }
1786
+ cron(expression) {
1787
+ if (this._mutateMethods.length === 0) {
1788
+ throw new Error("cron() must be called immediately after mutateMethod()");
1789
+ }
1790
+ const methods = [...this._mutateMethods];
1791
+ const lastIndex = methods.length - 1;
1792
+ methods[lastIndex] = { ...methods[lastIndex], cronExpression: expression };
1793
+ return new AggregateBuilder(this._name, this._id, this._schema, this._events, this._protections, methods, this._queryMethods);
1794
+ }
1795
+ clientQuery(methodName, handler) {
1796
+ const entry = {
1797
+ name: methodName,
1798
+ handler
1799
+ };
1800
+ return new AggregateBuilder(this._name, this._id, this._schema, this._events, this._protections, this._mutateMethods, [...this._queryMethods, entry]);
1801
+ }
1802
+ build() {
1803
+ const name = this._name;
1804
+ const id2 = this._id;
1805
+ const schema = this._schema;
1806
+ const events = this._events;
1807
+ const protections = this._protections;
1808
+ const mutateMethods = this._mutateMethods;
1809
+ const queryMethods = this._queryMethods;
1810
+ const cronMethods = [];
1811
+ for (const method of mutateMethods) {
1812
+ if (method.cronExpression) {
1813
+ cronMethods.push({
1814
+ aggregateName: name,
1815
+ methodName: method.name,
1816
+ cronExpression: method.cronExpression
1817
+ });
1818
+ }
1819
+ }
1820
+ const AggregateClass = class extends AggregateBase {
1821
+ static __aggregateName = name;
1822
+ static __aggregateId = id2;
1823
+ static __aggregateSchema = schema;
1824
+ static __aggregateEvents = events;
1825
+ static __aggregateProtections = protections;
1826
+ static __aggregateMutateMethods = mutateMethods;
1827
+ static __aggregateQueryMethods = queryMethods;
1828
+ static __aggregateCronMethods = cronMethods;
1829
+ static getEvent(eventName) {
1830
+ const entry = events.find((e) => e.isPublic && e.event.name === eventName);
1831
+ return entry?.event;
1832
+ }
1833
+ };
1834
+ Object.defineProperty(AggregateClass, "name", {
1835
+ value: `${name}Aggregate`
1836
+ });
1837
+ return AggregateClass;
1838
+ }
1839
+ }
1840
+ function aggregate(name, id2, schema) {
1841
+ const schemaObj = schema instanceof ArcObject ? schema : new ArcObject(schema);
1842
+ return new AggregateBuilder(name, id2, schemaObj, [], [], [], []);
1843
+ }
1844
+ // src/context-element/aggregate/aggregate-element.ts
1845
+ class ArcAggregateElement extends ArcContextElement {
1846
+ ctor;
1847
+ schema;
1848
+ constructor(ctor) {
1849
+ super(ctor.__aggregateName);
1850
+ this.ctor = ctor;
1851
+ this.schema = ctor.__aggregateSchema;
1852
+ }
1853
+ getHandlers() {
1854
+ const handlers = {};
1855
+ for (const entry of this.ctor.__aggregateEvents) {
1856
+ handlers[entry.event.name] = entry.handler;
1857
+ }
1858
+ return handlers;
1859
+ }
1860
+ getElements() {
1861
+ return this.ctor.__aggregateEvents.map((e) => e.event);
1862
+ }
1863
+ queryContext(adapters) {
1864
+ const privateQuery = this.buildPrivateQuery(adapters);
1865
+ const queryMethods = this.ctor.__aggregateQueryMethods ?? [];
1866
+ const auth = this.getAuth(adapters);
1867
+ const result = {};
1868
+ for (const method of queryMethods) {
1869
+ result[method.name] = (...args) => {
1870
+ const ctx = { $query: privateQuery, $auth: auth };
1871
+ return method.handler(ctx, ...args);
1872
+ };
1873
+ }
1874
+ return result;
1875
+ }
1876
+ buildPrivateQuery(adapters) {
1877
+ const viewName = this.ctor.__aggregateName;
1878
+ const protections = this.ctor.__aggregateProtections;
1879
+ const Ctor = this.ctor;
1880
+ const getReadRestrictions = () => {
1881
+ if (protections.length === 0)
1882
+ return null;
1883
+ if (!adapters.authAdapter)
1884
+ return false;
1885
+ const decoded = adapters.authAdapter.getDecoded();
1886
+ if (!decoded)
1887
+ return false;
1888
+ for (const protection of protections) {
1889
+ if (protection.token.name === decoded.tokenName) {
1890
+ const restrictions = protection.protectionFn(decoded.params);
1891
+ if (restrictions === false)
1892
+ return false;
1893
+ return restrictions ?? {};
1894
+ }
1895
+ }
1896
+ return false;
1897
+ };
1898
+ const applyRestrictions = (options) => {
1899
+ const restrictions = getReadRestrictions();
1900
+ if (restrictions === false)
1901
+ return false;
1902
+ if (!restrictions || Object.keys(restrictions).length === 0) {
1903
+ return options || {};
1904
+ }
1905
+ const where = { ...options?.where || {}, ...restrictions };
1906
+ return { ...options, where };
1907
+ };
1908
+ const findRows = async (options) => {
1909
+ if (adapters.dataStorage) {
1910
+ return adapters.dataStorage.getStore(viewName).find(options);
1911
+ }
1912
+ if (adapters.streamingCache) {
1913
+ return adapters.streamingCache.getStore(viewName).find(options);
1914
+ }
1915
+ if (adapters.queryWire) {
1916
+ return adapters.queryWire.query(viewName, options);
1917
+ }
1918
+ return [];
1919
+ };
1920
+ const schema = this.ctor.__aggregateSchema;
1921
+ const deserializeRow = (row) => {
1922
+ const { _id, ...rest } = row;
1923
+ return { _id, ...schema.deserialize(rest) };
1924
+ };
1925
+ return {
1926
+ find: async (options, mapper) => {
1927
+ const restricted = applyRestrictions(options);
1928
+ if (restricted === false)
1929
+ return [];
1930
+ const rows = await findRows(restricted);
1931
+ return mapper ? rows.map((row) => new mapper(row, adapters)) : rows.map(deserializeRow);
1932
+ },
1933
+ findOne: async (where, mapper) => {
1934
+ const restrictions = getReadRestrictions();
1935
+ if (restrictions === false)
1936
+ return;
1937
+ const mergedWhere = restrictions && Object.keys(restrictions).length > 0 ? { ...where, ...restrictions } : where;
1938
+ const rows = await findRows({ where: mergedWhere });
1939
+ const row = rows[0];
1940
+ if (!row)
1941
+ return;
1942
+ return mapper ? new mapper(row, adapters) : deserializeRow(row);
1943
+ }
1944
+ };
1945
+ }
1946
+ getAuth(adapters) {
1947
+ if (adapters.authAdapter) {
1948
+ const decoded = adapters.authAdapter.getDecoded();
1949
+ return decoded ? { params: decoded.params, tokenName: decoded.tokenName } : { params: {}, tokenName: "" };
1950
+ }
1951
+ return { params: {}, tokenName: "" };
1952
+ }
1953
+ mutateContext(adapters) {
1954
+ const events = this.ctor.__aggregateEvents;
1955
+ const privateQuery = this.buildPrivateQuery(adapters);
1956
+ const auth = this.getAuth(adapters);
1957
+ const buildMutateMethodCtx = () => {
1958
+ const ctx = {};
1959
+ for (const entry of events) {
1960
+ const arcEvent = entry.event;
1961
+ ctx[arcEvent.name] = {
1962
+ emit: async (payload) => {
1963
+ if (!adapters.eventPublisher) {
1964
+ throw new Error(`Cannot emit event "${arcEvent.name}": no eventPublisher adapter available`);
1965
+ }
1966
+ const eventInstance = {
1967
+ type: arcEvent.name,
1968
+ payload,
1969
+ id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
1970
+ createdAt: new Date
1971
+ };
1972
+ await adapters.eventPublisher.publish(eventInstance);
1973
+ }
1974
+ };
1975
+ }
1976
+ ctx.$auth = auth;
1977
+ ctx.$query = privateQuery;
1978
+ return ctx;
1979
+ };
1980
+ const result = {};
1981
+ for (const method of this.ctor.__aggregateMutateMethods ?? []) {
1982
+ result[method.name] = async (params) => {
1983
+ if (!method.handler) {
1984
+ if (!adapters.commandWire) {
1985
+ throw new Error(`Method "${method.name}" is server-only but no commandWire (WebSocket) is available.`);
1986
+ }
1987
+ const wireAuth = adapters.scope ? { scope: adapters.scope.scopeName, token: adapters.scope.getToken() } : undefined;
1988
+ return adapters.commandWire.executeCommand(`${this.ctor.__aggregateName}.${method.name}`, params, wireAuth);
1989
+ }
1990
+ const ctx = buildMutateMethodCtx();
1991
+ return method.handler(ctx, params);
1992
+ };
1993
+ }
1994
+ return result;
1995
+ }
1996
+ databaseStoreSchema() {
1997
+ const viewName = this.ctor.__aggregateName;
1998
+ const idSchema = new ArcObject({
1999
+ _id: new ArcString().primaryKey()
2000
+ });
2001
+ const viewSchema = idSchema.merge(this.ctor.__aggregateSchema.rawShape || {});
2002
+ const eventSchema = ArcEvent.sharedDatabaseStoreSchema();
2003
+ return {
2004
+ tables: [
2005
+ {
2006
+ name: viewName,
2007
+ schema: viewSchema
2008
+ },
2009
+ ...eventSchema.tables
2010
+ ]
2011
+ };
2012
+ }
2013
+ }
2014
+ // src/context-element/aggregate/index.ts
2015
+ function aggregateContextElement(ctor) {
2016
+ return new ArcAggregateElement(ctor);
2017
+ }
2018
+ // src/context-element/element-context.ts
2019
+ function buildElementContext(queryElements, mutationElements, adapters) {
2020
+ const context2 = {};
2021
+ const elementMap = new Map;
2022
+ for (const element of queryElements) {
2023
+ if (element.queryContext) {
2024
+ const ctx = element.queryContext(adapters);
2025
+ context2[element.name] = ctx;
2026
+ elementMap.set(element, ctx);
2027
+ }
2028
+ }
2029
+ for (const element of mutationElements) {
2030
+ if (element.mutateContext) {
2031
+ const ctx = element.mutateContext(adapters);
2032
+ context2[element.name] = ctx;
2033
+ elementMap.set(element, ctx);
2034
+ }
2035
+ }
2036
+ context2.get = (element) => {
2037
+ const cached = elementMap.get(element);
2038
+ if (cached)
2039
+ return cached;
2040
+ if (element.queryContext) {
2041
+ const ctx = element.queryContext(adapters);
2042
+ elementMap.set(element, ctx);
2043
+ return ctx;
2044
+ }
2045
+ if (element.mutateContext) {
2046
+ const ctx = element.mutateContext(adapters);
2047
+ elementMap.set(element, ctx);
2048
+ return ctx;
2049
+ }
2050
+ throw new Error(`Element "${element.name}" has no context available`);
2051
+ };
2052
+ context2.query = (element) => {
2053
+ if (!element.queryContext) {
2054
+ throw new Error(`Element "${element.name}" has no queryContext`);
2055
+ }
2056
+ return element.queryContext(adapters);
2057
+ };
2058
+ context2.mutate = (element) => {
2059
+ if (!element.mutateContext) {
2060
+ throw new Error(`Element "${element.name}" has no mutateContext`);
2061
+ }
2062
+ return element.mutateContext(adapters);
2063
+ };
2064
+ return context2;
2065
+ }
2066
+
2067
+ // src/context-element/command/command.ts
2068
+ class ArcCommand extends ArcContextElement {
2069
+ data;
2070
+ constructor(data) {
2071
+ super(data.name);
2072
+ this.data = data;
2073
+ }
2074
+ description(description) {
2075
+ return new ArcCommand({
2076
+ ...this.data,
2077
+ description
2078
+ });
2079
+ }
2080
+ get isPublic() {
2081
+ return !this.hasProtections;
2082
+ }
2083
+ query(elements) {
2084
+ return new ArcCommand({
2085
+ ...this.data,
2086
+ queryElements: elements
2087
+ });
2088
+ }
2089
+ mutate(elements) {
2090
+ return new ArcCommand({
2091
+ ...this.data,
2092
+ mutationElements: elements
2093
+ });
2094
+ }
2095
+ withParams(schema) {
2096
+ return new ArcCommand({
2097
+ ...this.data,
2098
+ params: schema instanceof ArcObject ? schema : new ArcObject(schema)
2099
+ });
2100
+ }
2101
+ withResult(...schemas) {
2102
+ return new ArcCommand({
2103
+ ...this.data,
2104
+ results: schemas
2105
+ });
2106
+ }
2107
+ handle(handler) {
2108
+ return new ArcCommand({
2109
+ ...this.data,
2110
+ handler
2111
+ });
2112
+ }
2113
+ protectBy(token, check) {
1619
2114
  const existingProtections = this.data.protections || [];
1620
- return new ArcEvent({
2115
+ return new ArcCommand({
1621
2116
  ...this.data,
1622
- protections: [...existingProtections, { token, protectionFn }]
2117
+ protections: [...existingProtections, { token, check }]
1623
2118
  });
1624
2119
  }
1625
2120
  get hasProtections() {
@@ -1628,67 +2123,86 @@ class ArcEvent extends ArcContextElement {
1628
2123
  get protections() {
1629
2124
  return this.data.protections || [];
1630
2125
  }
1631
- getProtectionFor(tokenInstance) {
1632
- if (!this.data.protections) {
1633
- return null;
2126
+ async verifyProtections(tokens) {
2127
+ if (!this.data.protections || this.data.protections.length === 0) {
2128
+ return true;
1634
2129
  }
1635
2130
  for (const protection of this.data.protections) {
1636
- if (tokenInstance.getTokenDefinition() === protection.token) {
1637
- return protection.protectionFn(tokenInstance.params);
2131
+ const tokenInstance = tokens.find((t) => t.getTokenDefinition() === protection.token);
2132
+ if (!tokenInstance) {
2133
+ return false;
2134
+ }
2135
+ const allowed = await protection.check(tokenInstance);
2136
+ if (!allowed) {
2137
+ return false;
1638
2138
  }
1639
2139
  }
1640
- return null;
2140
+ return true;
1641
2141
  }
1642
- tags(tagFields) {
1643
- return new ArcEvent({
1644
- ...this.data,
1645
- tagFields
1646
- });
2142
+ async init(environment, adapters) {
2143
+ if (environment === "server" && this.data.handler && adapters.commandWire?.registerCommandHandler) {
2144
+ adapters.commandWire.registerCommandHandler(this.data.name, async (params) => {
2145
+ const executeFunc = this.mutateContext(adapters);
2146
+ return await executeFunc(params);
2147
+ });
2148
+ }
1647
2149
  }
1648
- get tagFields() {
1649
- return this.data.tagFields || [];
2150
+ mutateContext(adapters) {
2151
+ const executeFunc = async (params) => {
2152
+ if (this.data.handler) {
2153
+ return await this.executeLocally(params, adapters);
2154
+ }
2155
+ if (!adapters.commandWire) {
2156
+ throw new Error(`Command "${this.data.name}" has no handler and no commandWire adapter available for remote execution`);
2157
+ }
2158
+ return await adapters.commandWire.executeCommand(this.data.name, params);
2159
+ };
2160
+ return Object.assign(executeFunc, { params: this.data.params });
1650
2161
  }
1651
- static _sharedSchema = null;
1652
- static sharedDatabaseStoreSchema() {
1653
- if (ArcEvent._sharedSchema) {
1654
- return ArcEvent._sharedSchema;
2162
+ async executeLocally(params, adapters) {
2163
+ if (!this.data.handler) {
2164
+ throw new Error(`Command "${this.data.name}" has no handler`);
1655
2165
  }
1656
- const eventsSchema = new ArcObject({
1657
- _id: new ArcString().primaryKey(),
1658
- type: new ArcString().index(),
1659
- payload: new ArcString,
1660
- createdAt: new ArcString().index()
1661
- });
1662
- const tagsSchema = new ArcObject({
1663
- _id: new ArcString().primaryKey(),
1664
- eventId: new ArcString().index(),
1665
- tagKey: new ArcString().index(),
1666
- tagValue: new ArcString().index()
1667
- });
1668
- const syncStatusSchema = new ArcObject({
1669
- _id: new ArcString().primaryKey(),
1670
- eventId: new ArcString().index(),
1671
- synced: new ArcBoolean().index(),
1672
- timestamp: new ArcNumber,
1673
- retryCount: new ArcNumber
1674
- });
1675
- ArcEvent._sharedSchema = {
1676
- tables: [
1677
- { name: "events", schema: eventsSchema },
1678
- { name: "event_tags", schema: tagsSchema },
1679
- { name: "event_sync_status", schema: syncStatusSchema }
1680
- ]
1681
- };
1682
- return ArcEvent._sharedSchema;
2166
+ const context2 = this.buildCommandContext(adapters);
2167
+ const result = await this.data.handler(context2, params);
2168
+ return result;
1683
2169
  }
1684
- databaseStoreSchema() {
1685
- return ArcEvent.sharedDatabaseStoreSchema();
2170
+ buildCommandContext(adapters) {
2171
+ const context2 = buildElementContext(this.data.queryElements, this.data.mutationElements, adapters);
2172
+ if (this.hasProtections && adapters.authAdapter) {
2173
+ const decoded = adapters.authAdapter.getDecoded();
2174
+ if (decoded) {
2175
+ context2.$auth = {
2176
+ params: decoded.params,
2177
+ tokenName: decoded.tokenName
2178
+ };
2179
+ } else {
2180
+ throw new Error(`Command "${this.data.name}" requires authentication but no valid token found`);
2181
+ }
2182
+ }
2183
+ return context2;
2184
+ }
2185
+ toJsonSchema() {
2186
+ const parametersSchema = this.data.params ? this.data.params.toJsonSchema?.() ?? {
2187
+ type: "object",
2188
+ properties: {}
2189
+ } : { type: "object", properties: {} };
2190
+ return {
2191
+ type: "function",
2192
+ name: this.data.name,
2193
+ description: this.data.description ?? undefined,
2194
+ parameters: parametersSchema,
2195
+ strict: true
2196
+ };
1686
2197
  }
1687
2198
  }
1688
- function event(name, payload) {
1689
- return new ArcEvent({
2199
+ function command(name) {
2200
+ return new ArcCommand({
1690
2201
  name,
1691
- payload: payload ? payload instanceof ArcObject ? payload : new ArcObject(payload) : undefined,
2202
+ params: null,
2203
+ results: [],
2204
+ queryElements: [],
2205
+ mutationElements: [],
1692
2206
  protections: []
1693
2207
  });
1694
2208
  }
@@ -1941,38 +2455,7 @@ class ArcRoute extends ArcContextElement {
1941
2455
  return true;
1942
2456
  }
1943
2457
  buildContext(adapters, authParams) {
1944
- const context2 = {};
1945
- const elementMap = new Map;
1946
- for (const element of this.data.queryElements) {
1947
- if (element.queryContext) {
1948
- const elementContext = element.queryContext(adapters);
1949
- context2[element.name] = elementContext;
1950
- elementMap.set(element, elementContext);
1951
- }
1952
- }
1953
- for (const element of this.data.mutationElements) {
1954
- if (element.mutateContext) {
1955
- const elementContext = element.mutateContext(adapters);
1956
- context2[element.name] = elementContext;
1957
- elementMap.set(element, elementContext);
1958
- }
1959
- }
1960
- context2.get = (element) => {
1961
- const cached = elementMap.get(element);
1962
- if (cached)
1963
- return cached;
1964
- if (element.queryContext) {
1965
- const ctx = element.queryContext(adapters);
1966
- elementMap.set(element, ctx);
1967
- return ctx;
1968
- }
1969
- if (element.mutateContext) {
1970
- const ctx = element.mutateContext(adapters);
1971
- elementMap.set(element, ctx);
1972
- return ctx;
1973
- }
1974
- throw new Error(`Element "${element.name}" has no context available`);
1975
- };
2458
+ const context2 = buildElementContext(this.data.queryElements, this.data.mutationElements, adapters);
1976
2459
  if (authParams) {
1977
2460
  context2.$auth = authParams;
1978
2461
  }
@@ -2394,6 +2877,9 @@ class StoreState {
2394
2877
  applySerializedChanges(changes) {
2395
2878
  return Promise.all(changes.map((change) => this.applyChange(change)));
2396
2879
  }
2880
+ get listenerCount() {
2881
+ return this.listeners.size;
2882
+ }
2397
2883
  unsubscribe(listener4) {
2398
2884
  this.listeners.delete(listener4);
2399
2885
  }
@@ -3621,12 +4107,144 @@ class ArcStringEnum extends ArcAbstract {
3621
4107
  function stringEnum(...values) {
3622
4108
  return new ArcStringEnum(values);
3623
4109
  }
4110
+ // src/model/context-accessor.ts
4111
+ function buildContextAccessor(context2, adapters, contextMethod, onCall) {
4112
+ const result = {};
4113
+ for (const element2 of context2.elements) {
4114
+ const ctxFn = element2[contextMethod];
4115
+ if (typeof ctxFn !== "function")
4116
+ continue;
4117
+ const methods = ctxFn.call(element2, adapters);
4118
+ if (!methods || typeof methods !== "object")
4119
+ continue;
4120
+ const wrapped = {};
4121
+ for (const [methodName, method] of Object.entries(methods)) {
4122
+ if (typeof method !== "function")
4123
+ continue;
4124
+ wrapped[methodName] = (...args) => onCall({ element: element2.name, method: methodName, args }, () => method(...args));
4125
+ }
4126
+ result[element2.name] = wrapped;
4127
+ }
4128
+ return result;
4129
+ }
4130
+ function executeDescriptor(descriptor, context2, adapters, contextMethod) {
4131
+ const element2 = context2.get(descriptor.element);
4132
+ if (!element2) {
4133
+ throw new Error(`Element '${descriptor.element}' not found in context`);
4134
+ }
4135
+ const ctxFn = element2[contextMethod];
4136
+ if (typeof ctxFn !== "function") {
4137
+ throw new Error(`Element '${descriptor.element}' has no ${contextMethod}`);
4138
+ }
4139
+ const methods = ctxFn.call(element2, adapters);
4140
+ const method = methods?.[descriptor.method];
4141
+ if (typeof method !== "function") {
4142
+ throw new Error(`Method '${descriptor.method}' not found on '${descriptor.element}'`);
4143
+ }
4144
+ return method(...descriptor.args);
4145
+ }
4146
+
4147
+ // src/model/scoped-model.ts
4148
+ class ScopedModel {
4149
+ context;
4150
+ scopeName;
4151
+ parent;
4152
+ authAdapter;
4153
+ scopedAdapters;
4154
+ tokenListeners = new Set;
4155
+ constructor(parent, scopeName) {
4156
+ this.parent = parent;
4157
+ this.context = parent.context;
4158
+ this.scopeName = scopeName;
4159
+ this.authAdapter = new AuthAdapter;
4160
+ this.scopedAdapters = {
4161
+ ...parent.getAdapters(),
4162
+ authAdapter: this.authAdapter,
4163
+ scope: this
4164
+ };
4165
+ }
4166
+ setToken(token) {
4167
+ this.authAdapter.setToken(token);
4168
+ const eventWire = this.parent.getAdapters().eventWire;
4169
+ if (eventWire) {
4170
+ eventWire.setScopeToken(this.scopeName, token);
4171
+ if (token && eventWire.getState() === "disconnected") {
4172
+ eventWire.connect();
4173
+ }
4174
+ }
4175
+ for (const listener4 of this.tokenListeners) {
4176
+ listener4();
4177
+ }
4178
+ }
4179
+ getToken() {
4180
+ return this.authAdapter.getToken();
4181
+ }
4182
+ getDecoded() {
4183
+ return this.authAdapter.getDecoded();
4184
+ }
4185
+ getParams() {
4186
+ return this.authAdapter.getParams();
4187
+ }
4188
+ isAuthenticated() {
4189
+ return this.authAdapter.isAuthenticated();
4190
+ }
4191
+ onTokenChange(listener4) {
4192
+ this.tokenListeners.add(listener4);
4193
+ return () => {
4194
+ this.tokenListeners.delete(listener4);
4195
+ };
4196
+ }
4197
+ getAdapters() {
4198
+ return this.scopedAdapters;
4199
+ }
4200
+ getEnvironment() {
4201
+ return this.parent.getEnvironment();
4202
+ }
4203
+ getAuth() {
4204
+ return { scope: this.scopeName, token: this.getToken() };
4205
+ }
4206
+ executeCommand(name, params) {
4207
+ const wire = this.parent.getAdapters().commandWire;
4208
+ if (!wire) {
4209
+ throw new Error(`Cannot execute command "${name}": no commandWire available.`);
4210
+ }
4211
+ return wire.executeCommand(name, params, this.getAuth());
4212
+ }
4213
+ remoteQuery(viewName, options) {
4214
+ const wire = this.parent.getAdapters().queryWire;
4215
+ if (!wire) {
4216
+ throw new Error(`Cannot query "${viewName}": no queryWire available.`);
4217
+ }
4218
+ return wire.query(viewName, options, this.getAuth());
4219
+ }
4220
+ subscribeQuery(descriptor, callback) {
4221
+ const wire = this.parent.getAdapters().eventWire;
4222
+ if (!wire) {
4223
+ throw new Error(`Cannot subscribe to query: no eventWire available.`);
4224
+ }
4225
+ return wire.subscribeQuery(descriptor, callback, this.scopeName);
4226
+ }
4227
+ get query() {
4228
+ return buildContextAccessor(this.context, this.scopedAdapters, "queryContext", (descriptor) => descriptor);
4229
+ }
4230
+ get command() {
4231
+ return buildContextAccessor(this.context, this.scopedAdapters, "mutateContext", (descriptor) => descriptor);
4232
+ }
4233
+ callQuery(descriptor) {
4234
+ return executeDescriptor(descriptor, this.context, this.scopedAdapters, "queryContext");
4235
+ }
4236
+ callCommand(descriptor) {
4237
+ return executeDescriptor(descriptor, this.context, this.scopedAdapters, "mutateContext");
4238
+ }
4239
+ }
4240
+
3624
4241
  // src/model/model.ts
3625
4242
  class Model {
3626
4243
  context;
3627
4244
  adapters;
3628
4245
  environment;
3629
4246
  initialized = false;
4247
+ scopes = new Map;
3630
4248
  constructor(context2, options) {
3631
4249
  this.context = context2;
3632
4250
  this.adapters = options.adapters;
@@ -3653,6 +4271,14 @@ class Model {
3653
4271
  getEnvironment() {
3654
4272
  return this.environment;
3655
4273
  }
4274
+ scope(name) {
4275
+ let s = this.scopes.get(name);
4276
+ if (!s) {
4277
+ s = new ScopedModel(this, name);
4278
+ this.scopes.set(name, s);
4279
+ }
4280
+ return s;
4281
+ }
3656
4282
  }
3657
4283
  // src/model/mutation-executor/mutation-executor.ts
3658
4284
  function mutationExecutor(model) {
@@ -3675,6 +4301,8 @@ class StreamingQueryCache {
3675
4301
  stores = new Map;
3676
4302
  views = [];
3677
4303
  activeStreams = new Map;
4304
+ pendingUnsubscribes = new Map;
4305
+ static UNSUBSCRIBE_DELAY_MS = 5000;
3678
4306
  registerViews(views) {
3679
4307
  this.views = views;
3680
4308
  for (const view3 of views) {
@@ -3697,6 +4325,11 @@ class StreamingQueryCache {
3697
4325
  return this.activeStreams.has(viewName);
3698
4326
  }
3699
4327
  registerStream(viewName, createStream) {
4328
+ const pending = this.pendingUnsubscribes.get(viewName);
4329
+ if (pending) {
4330
+ clearTimeout(pending);
4331
+ this.pendingUnsubscribes.delete(viewName);
4332
+ }
3700
4333
  const existing = this.activeStreams.get(viewName);
3701
4334
  if (existing) {
3702
4335
  existing.refCount++;
@@ -3721,14 +4354,37 @@ class StreamingQueryCache {
3721
4354
  return;
3722
4355
  stream.refCount--;
3723
4356
  if (stream.refCount <= 0) {
3724
- stream.unsubscribe();
3725
- this.activeStreams.delete(viewName);
4357
+ const timeout = setTimeout(() => {
4358
+ this.pendingUnsubscribes.delete(viewName);
4359
+ const current = this.activeStreams.get(viewName);
4360
+ if (current && current.refCount <= 0) {
4361
+ current.unsubscribe();
4362
+ this.activeStreams.delete(viewName);
4363
+ }
4364
+ }, StreamingQueryCache.UNSUBSCRIBE_DELAY_MS);
4365
+ this.pendingUnsubscribes.set(viewName, timeout);
3726
4366
  }
3727
4367
  }
3728
- setViewData(viewName, items) {
4368
+ subscribeQuery(descriptor, eventWire, scope) {
4369
+ const key = descriptor.element;
4370
+ const { unsubscribe } = this.registerStream(key, () => {
4371
+ const subId = eventWire.subscribeQuery(descriptor, (data) => {
4372
+ this.setViewData(descriptor.element, data);
4373
+ }, scope);
4374
+ return { unsubscribe: () => eventWire.unsubscribeQuery(subId) };
4375
+ });
4376
+ return unsubscribe;
4377
+ }
4378
+ setViewData(viewName, data) {
3729
4379
  const store = this.stores.get(viewName);
3730
- if (store) {
3731
- store.setAll(items);
4380
+ if (!store)
4381
+ return;
4382
+ if (Array.isArray(data)) {
4383
+ store.setAll(data);
4384
+ } else if (data && typeof data === "object" && "_id" in data) {
4385
+ store.setAll([data]);
4386
+ } else {
4387
+ store.setAll([]);
3732
4388
  }
3733
4389
  }
3734
4390
  async applyEvent(event3) {
@@ -3775,35 +4431,39 @@ class StreamingQueryCache {
3775
4431
  class StreamingStore {
3776
4432
  data = new Map;
3777
4433
  listeners = new Set;
4434
+ initialized = false;
3778
4435
  hasData() {
3779
- return this.data.size > 0;
4436
+ return this.initialized;
3780
4437
  }
3781
4438
  setAll(items) {
4439
+ this.initialized = true;
3782
4440
  this.data.clear();
3783
4441
  for (const item of items) {
3784
4442
  this.data.set(item._id, item);
3785
4443
  }
3786
- this.notifyListeners();
4444
+ this.notifyListeners(null);
3787
4445
  }
3788
4446
  set(id3, item) {
3789
4447
  this.data.set(id3, item);
3790
- this.notifyListeners();
4448
+ this.notifyListeners([{ type: "set", id: id3, item }]);
3791
4449
  }
3792
4450
  modify(id3, updates) {
3793
4451
  const existing = this.data.get(id3);
3794
4452
  if (existing) {
3795
- this.data.set(id3, { ...existing, ...updates });
3796
- this.notifyListeners();
4453
+ const updated = { ...existing, ...updates };
4454
+ this.data.set(id3, updated);
4455
+ this.notifyListeners([{ type: "set", id: id3, item: updated }]);
3797
4456
  }
3798
4457
  }
3799
4458
  remove(id3) {
3800
4459
  if (this.data.delete(id3)) {
3801
- this.notifyListeners();
4460
+ this.notifyListeners([{ type: "delete", id: id3, item: null }]);
3802
4461
  }
3803
4462
  }
3804
4463
  clear() {
4464
+ this.initialized = false;
3805
4465
  this.data.clear();
3806
- this.notifyListeners();
4466
+ this.notifyListeners(null);
3807
4467
  }
3808
4468
  find(options = {}) {
3809
4469
  let results = Array.from(this.data.values());
@@ -3822,9 +4482,9 @@ class StreamingStore {
3822
4482
  this.listeners.delete(listener4);
3823
4483
  };
3824
4484
  }
3825
- notifyListeners() {
4485
+ notifyListeners(events) {
3826
4486
  for (const listener4 of this.listeners) {
3827
- listener4();
4487
+ listener4(events);
3828
4488
  }
3829
4489
  }
3830
4490
  }
@@ -3893,126 +4553,6 @@ class StreamingEventPublisher {
3893
4553
  }
3894
4554
  }
3895
4555
  }
3896
- // src/streaming/streaming-live-query.ts
3897
- function isStaticView(element2) {
3898
- return element2 && "items" in element2 && !("getHandlers" in element2);
3899
- }
3900
- function streamingLiveQuery(model, queryFn, callback, options) {
3901
- const { queryWire, cache, authToken } = options;
3902
- let currentResult = undefined;
3903
- const unsubscribers = [];
3904
- const queriedViews = new Set;
3905
- const staticViews = new Set;
3906
- const queryContext = new Proxy({}, {
3907
- get(_target, viewName) {
3908
- const element2 = model.context.get(viewName);
3909
- if (!element2) {
3910
- throw new Error(`View '${viewName}' not found in context`);
3911
- }
3912
- if (isStaticView(element2)) {
3913
- staticViews.add(viewName);
3914
- return element2.queryContext(model.getAdapters());
3915
- }
3916
- queriedViews.add(viewName);
3917
- return {
3918
- find: async (findOptions = {}) => {
3919
- const store = cache.getStore(viewName);
3920
- return store.find(findOptions);
3921
- },
3922
- findOne: async (where) => {
3923
- const store = cache.getStore(viewName);
3924
- return store.findOne(where);
3925
- }
3926
- };
3927
- }
3928
- });
3929
- const executeQuery = async () => {
3930
- const result = await queryFn(queryContext);
3931
- currentResult = result;
3932
- callback(result);
3933
- };
3934
- const setupStreams = async () => {
3935
- queriedViews.clear();
3936
- staticViews.clear();
3937
- await queryFn(queryContext);
3938
- if (queriedViews.size === 0 && staticViews.size > 0) {
3939
- executeQuery();
3940
- return;
3941
- }
3942
- for (const viewName of queriedViews) {
3943
- const store = cache.getStore(viewName);
3944
- const cacheUnsub = store.subscribe(() => {
3945
- executeQuery();
3946
- });
3947
- unsubscribers.push(cacheUnsub);
3948
- const { unsubscribe: streamUnsub, wasReused } = cache.registerStream(viewName, () => {
3949
- return queryWire.stream(viewName, {}, (data) => {
3950
- cache.setViewData(viewName, data);
3951
- }, authToken);
3952
- });
3953
- unsubscribers.push(streamUnsub);
3954
- if (wasReused && cache.hasData(viewName)) {
3955
- executeQuery();
3956
- }
3957
- }
3958
- };
3959
- setupStreams();
3960
- return {
3961
- get result() {
3962
- return currentResult;
3963
- },
3964
- unsubscribe: () => {
3965
- for (const unsub of unsubscribers) {
3966
- unsub();
3967
- }
3968
- }
3969
- };
3970
- }
3971
- // src/model/live-query/live-query.ts
3972
- function liveQuery(model, queryFn, callback) {
3973
- let currentResult = undefined;
3974
- const adapters = model.getAdapters();
3975
- if (!adapters.dataStorage && adapters.streamingCache && adapters.queryWire) {
3976
- return streamingLiveQuery(model, queryFn, callback, {
3977
- queryWire: adapters.queryWire,
3978
- cache: adapters.streamingCache,
3979
- authToken: adapters.authAdapter?.getToken?.() ?? null
3980
- });
3981
- }
3982
- if (!adapters.dataStorage && !adapters.queryWire) {
3983
- throw new Error("liveQuery requires dataStorage or queryWire adapter");
3984
- }
3985
- const observableStorage = adapters.dataStorage ? observeQueries(adapters.dataStorage, () => {
3986
- executeQuery();
3987
- }) : null;
3988
- const observableAdapters = {
3989
- ...adapters,
3990
- dataStorage: observableStorage ?? undefined
3991
- };
3992
- const queryContext = new Proxy({}, {
3993
- get(_target, viewName) {
3994
- const element2 = model.context.get(viewName);
3995
- if (!element2) {
3996
- throw new Error(`View '${viewName}' not found in context`);
3997
- }
3998
- return element2.queryContext(observableAdapters);
3999
- }
4000
- });
4001
- const executeQuery = async () => {
4002
- const result = await queryFn(queryContext);
4003
- currentResult = result;
4004
- callback(result);
4005
- };
4006
- Promise.resolve().then(() => executeQuery());
4007
- return {
4008
- get result() {
4009
- return currentResult;
4010
- },
4011
- unsubscribe: () => {
4012
- observableStorage?.clear();
4013
- }
4014
- };
4015
- }
4016
4556
  // src/token/secured-data-storage.ts
4017
4557
  class SecuredStoreState {
4018
4558
  store;
@@ -4396,7 +4936,6 @@ export {
4396
4936
  token,
4397
4937
  stringEnum,
4398
4938
  string,
4399
- streamingLiveQuery,
4400
4939
  staticView,
4401
4940
  secureDataStorage,
4402
4941
  route,
@@ -4408,11 +4947,11 @@ export {
4408
4947
  number,
4409
4948
  mutationExecutor,
4410
4949
  murmurHash,
4411
- liveQuery,
4412
4950
  listener,
4413
4951
  id,
4414
4952
  file,
4415
4953
  extractDatabaseAgnosticSchema,
4954
+ executeDescriptor,
4416
4955
  event,
4417
4956
  deepMerge,
4418
4957
  date,
@@ -4421,11 +4960,15 @@ export {
4421
4960
  context,
4422
4961
  command,
4423
4962
  checkItemMatchesWhere,
4963
+ buildElementContext,
4964
+ buildContextAccessor,
4424
4965
  boolean,
4425
4966
  blob,
4426
4967
  array,
4427
4968
  applyOrderByAndLimit,
4428
4969
  any,
4970
+ aggregateContextElement,
4971
+ aggregate,
4429
4972
  Wire,
4430
4973
  TokenInstance,
4431
4974
  TokenCache,
@@ -4434,6 +4977,7 @@ export {
4434
4977
  StoreState,
4435
4978
  SecuredStoreState,
4436
4979
  SecuredDataStorage,
4980
+ ScopedModel,
4437
4981
  QueryWire,
4438
4982
  ObservableDataStorage,
4439
4983
  Model,
@@ -4460,6 +5004,7 @@ export {
4460
5004
  ArcNumber,
4461
5005
  ArcListener,
4462
5006
  ArcId,
5007
+ ArcFragmentBase,
4463
5008
  ArcFile,
4464
5009
  ArcEvent,
4465
5010
  ArcDate,
@@ -4472,5 +5017,8 @@ export {
4472
5017
  ArcBlob,
4473
5018
  ArcArray,
4474
5019
  ArcAny,
4475
- ArcAbstract
5020
+ ArcAggregateElement,
5021
+ ArcAbstract,
5022
+ AggregateBuilder,
5023
+ AggregateBase
4476
5024
  };