@arcote.tech/arc-host 0.7.13 → 0.7.15

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/dist/index.js CHANGED
@@ -4677,88 +4677,6 @@ function queryHandler(ch) {
4677
4677
  }
4678
4678
  };
4679
4679
  }
4680
- var streamIdCounter = 0;
4681
- var streamConnections = new Map;
4682
- function streamHandler(ch) {
4683
- return (_req, url, ctx) => {
4684
- if (!url.pathname.startsWith("/stream/") || _req.method !== "GET")
4685
- return null;
4686
- const viewName = url.pathname.split("/stream/")[1];
4687
- if (!viewName) {
4688
- return new Response("Invalid stream path", {
4689
- status: 400,
4690
- headers: ctx.corsHeaders
4691
- });
4692
- }
4693
- const findOptions = {};
4694
- const whereParam = url.searchParams.get("where");
4695
- if (whereParam) {
4696
- try {
4697
- findOptions.where = JSON.parse(whereParam);
4698
- } catch {
4699
- return new Response("Invalid 'where' parameter", {
4700
- status: 400,
4701
- headers: ctx.corsHeaders
4702
- });
4703
- }
4704
- }
4705
- const orderByParam = url.searchParams.get("orderBy");
4706
- if (orderByParam) {
4707
- try {
4708
- findOptions.orderBy = JSON.parse(orderByParam);
4709
- } catch {
4710
- return new Response("Invalid 'orderBy' parameter", {
4711
- status: 400,
4712
- headers: ctx.corsHeaders
4713
- });
4714
- }
4715
- }
4716
- const limitParam = url.searchParams.get("limit");
4717
- if (limitParam)
4718
- findOptions.limit = parseInt(limitParam, 10);
4719
- const streamId = `stream_${++streamIdCounter}_${Date.now()}`;
4720
- const rawToken = ctx.rawToken;
4721
- const stream = new ReadableStream({
4722
- start(controller) {
4723
- const scoped = new ScopedModel2(ch.getModel(), "stream");
4724
- if (rawToken)
4725
- scoped.setToken(rawToken);
4726
- const descriptor = { element: viewName, method: "find", args: [findOptions] };
4727
- const sendData = async () => {
4728
- try {
4729
- const data = await scoped.callQuery(descriptor);
4730
- controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: "data", data })}
4731
-
4732
- `));
4733
- } catch {
4734
- unsubscribe();
4735
- }
4736
- };
4737
- sendData();
4738
- const unsubscribe = ch.getEventPublisher().subscribe("*", () => sendData());
4739
- streamConnections.set(streamId, { id: streamId, controller, unsubscribe });
4740
- controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: "connected", streamId })}
4741
-
4742
- `));
4743
- },
4744
- cancel() {
4745
- const conn = streamConnections.get(streamId);
4746
- if (conn) {
4747
- conn.unsubscribe();
4748
- streamConnections.delete(streamId);
4749
- }
4750
- }
4751
- });
4752
- return new Response(stream, {
4753
- headers: {
4754
- ...ctx.corsHeaders,
4755
- "Content-Type": "text/event-stream",
4756
- "Cache-Control": "no-cache",
4757
- Connection: "keep-alive"
4758
- }
4759
- });
4760
- };
4761
- }
4762
4680
  function eventSyncHandler(ch) {
4763
4681
  return async (req, url, ctx) => {
4764
4682
  if (url.pathname !== "/sync/events" || req.method !== "POST")
@@ -4859,25 +4777,71 @@ function arcHttpHandlers(ch, cm) {
4859
4777
  healthHandler(cm),
4860
4778
  commandHandler(ch),
4861
4779
  queryHandler(ch),
4862
- streamHandler(ch),
4863
4780
  eventSyncHandler(ch),
4864
4781
  routeHandler(ch)
4865
4782
  ];
4866
4783
  }
4867
- function cleanupStreams() {
4868
- for (const conn of streamConnections.values())
4869
- conn.unsubscribe();
4870
- streamConnections.clear();
4871
- }
4872
4784
  // src/middleware/ws.ts
4873
- import { ScopedModel as ScopedModel3 } from "@arcote.tech/arc";
4874
- var clientViewSubs = new Map;
4785
+ import {
4786
+ ScopedModel as ScopedModel3,
4787
+ ScopedStore,
4788
+ checkItemMatchesWhere
4789
+ } from "@arcote.tech/arc";
4790
+ var viewSubscribers = new Map;
4875
4791
  function cleanupClientSubs(clientId) {
4876
- const subs = clientViewSubs.get(clientId);
4877
- if (subs) {
4878
- for (const unsub of subs.values())
4879
- unsub();
4880
- clientViewSubs.delete(clientId);
4792
+ const prefix = `${clientId}:`;
4793
+ for (const subs of viewSubscribers.values()) {
4794
+ for (const key of subs.keys()) {
4795
+ if (key.startsWith(prefix))
4796
+ subs.delete(key);
4797
+ }
4798
+ }
4799
+ }
4800
+ function filterChangeForSubscriber(change, restrictions) {
4801
+ const newMatches = change.newRow !== null && (restrictions === null || checkItemMatchesWhere(change.newRow, restrictions));
4802
+ if (newMatches) {
4803
+ return { type: "set", id: change.id, item: change.newRow };
4804
+ }
4805
+ const oldMatches = change.oldRow !== null && (restrictions === null || checkItemMatchesWhere(change.oldRow, restrictions));
4806
+ if (oldMatches) {
4807
+ return { type: "delete", id: change.id, item: null };
4808
+ }
4809
+ return null;
4810
+ }
4811
+ function broadcastViewChanges(committed, connectionManager) {
4812
+ const byStore = new Map;
4813
+ for (const change of committed) {
4814
+ let list = byStore.get(change.store);
4815
+ if (!list) {
4816
+ list = [];
4817
+ byStore.set(change.store, list);
4818
+ }
4819
+ list.push(change);
4820
+ }
4821
+ for (const [viewName, changes] of byStore) {
4822
+ const subs = viewSubscribers.get(viewName);
4823
+ if (!subs || subs.size === 0)
4824
+ continue;
4825
+ for (const sub of subs.values()) {
4826
+ const filtered = [];
4827
+ for (const change of changes) {
4828
+ const wireChange = filterChangeForSubscriber(change, sub.restrictions);
4829
+ if (wireChange)
4830
+ filtered.push(wireChange);
4831
+ }
4832
+ if (filtered.length === 0)
4833
+ continue;
4834
+ if (sub.buffer) {
4835
+ sub.buffer.push(...filtered);
4836
+ continue;
4837
+ }
4838
+ connectionManager.sendToClient(sub.clientId, {
4839
+ type: "view-changes",
4840
+ element: viewName,
4841
+ scope: sub.scope,
4842
+ changes: filtered
4843
+ });
4844
+ }
4881
4845
  }
4882
4846
  }
4883
4847
  function scopeAuthHandler() {
@@ -4903,6 +4867,8 @@ function syncEventsHandler() {
4903
4867
  for (const c of ctx.connectionManager.getAllClients()) {
4904
4868
  if (c.id === client.id)
4905
4869
  continue;
4870
+ if (!c.wantsEventSync)
4871
+ continue;
4906
4872
  const clientTokens = ctx.connectionManager.getAllScopeTokens(c.id);
4907
4873
  const authorized = filterEventsForTokens(clientTokens, persisted, ctx.contextHandler.getEventDefinitions());
4908
4874
  if (authorized.length > 0) {
@@ -4925,6 +4891,7 @@ function requestSyncHandler() {
4925
4891
  return async (client, message, ctx) => {
4926
4892
  if (message.type !== "request-sync")
4927
4893
  return false;
4894
+ client.wantsEventSync = true;
4928
4895
  const allTokens = ctx.connectionManager.getAllScopeTokens(client.id);
4929
4896
  const token = allTokens.length > 0 ? allTokens[0] : null;
4930
4897
  const events = await ctx.contextHandler.getEventsSince(message.lastHostEventId, token);
@@ -4973,74 +4940,82 @@ function executeCommandHandler() {
4973
4940
  return true;
4974
4941
  };
4975
4942
  }
4976
- function querySubscriptionHandler() {
4943
+ function viewSubscriptionHandler() {
4977
4944
  return async (client, message, ctx) => {
4978
- if (message.type === "subscribe-query") {
4979
- const { subscriptionId, descriptor, scope } = message;
4980
- const scopeToken = scope ? client.scopeTokens.get(scope) : null;
4945
+ if (message.type === "subscribe-view") {
4946
+ const { element: elementName } = message;
4947
+ const scope = message.scope ?? "default";
4948
+ const scopeToken = client.scopeTokens.get(scope) ?? null;
4981
4949
  let rawToken = scopeToken?.raw ?? null;
4982
4950
  if (!rawToken && client.scopeTokens.size > 0) {
4983
4951
  rawToken = client.scopeTokens.values().next().value.raw;
4984
4952
  }
4985
- const scoped = new ScopedModel3(ctx.contextHandler.getModel(), scope ?? "default");
4953
+ const scoped = new ScopedModel3(ctx.contextHandler.getModel(), scope);
4986
4954
  if (rawToken)
4987
4955
  scoped.setToken(rawToken);
4988
- let lastResultJson = "";
4989
- const sendData = async () => {
4990
- try {
4991
- const data = await scoped.callQuery(descriptor);
4992
- const json = JSON.stringify(data ?? null);
4993
- if (json === lastResultJson)
4994
- return;
4995
- lastResultJson = json;
4996
- ctx.connectionManager.sendToClient(client.id, {
4997
- type: "query-data",
4998
- subscriptionId,
4999
- data: data ?? null
5000
- });
5001
- } catch (err) {
5002
- console.error(`[Arc] Query subscription error:`, err);
5003
- }
5004
- };
5005
- sendData();
5006
- const element = ctx.contextHandler.getModel().context.get(descriptor.element);
5007
- const handlers = element?.getHandlers?.();
5008
- const eventTypes = handlers ? Object.keys(handlers) : [];
5009
- let debounceTimer;
5010
- const debouncedSend = () => {
5011
- if (debounceTimer)
5012
- clearTimeout(debounceTimer);
5013
- debounceTimer = setTimeout(() => sendData(), 50);
5014
- };
5015
- const unsubscribes = [];
5016
- if (eventTypes.length > 0) {
5017
- for (const eventType of eventTypes) {
5018
- unsubscribes.push(ctx.contextHandler.getEventPublisher().subscribe(eventType, debouncedSend));
5019
- }
5020
- } else {
5021
- unsubscribes.push(ctx.contextHandler.getEventPublisher().subscribe("*", debouncedSend));
4956
+ const element = ctx.contextHandler.getModel().context.get(elementName);
4957
+ if (!element || typeof element.getRestrictionsFor !== "function") {
4958
+ ctx.connectionManager.sendToClient(client.id, {
4959
+ type: "error",
4960
+ message: `Cannot subscribe to view "${elementName}": unknown element`
4961
+ });
4962
+ return true;
5022
4963
  }
5023
- const cleanup = () => {
5024
- if (debounceTimer)
5025
- clearTimeout(debounceTimer);
5026
- for (const unsub of unsubscribes)
5027
- unsub();
4964
+ const { restrictions, denied } = element.getRestrictionsFor(scoped.getAdapters());
4965
+ const subKey = `${client.id}:${scope}`;
4966
+ if (denied) {
4967
+ viewSubscribers.get(elementName)?.delete(subKey);
4968
+ ctx.connectionManager.sendToClient(client.id, {
4969
+ type: "view-snapshot",
4970
+ element: elementName,
4971
+ scope,
4972
+ items: []
4973
+ });
4974
+ return true;
4975
+ }
4976
+ const subscriber = {
4977
+ clientId: client.id,
4978
+ scope,
4979
+ restrictions,
4980
+ buffer: []
5028
4981
  };
5029
- if (!clientViewSubs.has(client.id)) {
5030
- clientViewSubs.set(client.id, new Map);
4982
+ if (!viewSubscribers.has(elementName)) {
4983
+ viewSubscribers.set(elementName, new Map);
4984
+ }
4985
+ viewSubscribers.get(elementName).set(subKey, subscriber);
4986
+ try {
4987
+ const store = ctx.contextHandler.getDataStorage().getStore(elementName);
4988
+ const items = restrictions ? await new ScopedStore(store, restrictions, false).find({}) : await store.find({});
4989
+ ctx.connectionManager.sendToClient(client.id, {
4990
+ type: "view-snapshot",
4991
+ element: elementName,
4992
+ scope,
4993
+ items
4994
+ });
4995
+ } catch (err) {
4996
+ viewSubscribers.get(elementName)?.delete(subKey);
4997
+ console.error(`[Arc] View subscription error (${elementName}):`, err);
4998
+ ctx.connectionManager.sendToClient(client.id, {
4999
+ type: "error",
5000
+ message: `Failed to subscribe to view "${elementName}"`
5001
+ });
5002
+ return true;
5003
+ }
5004
+ const buffered = subscriber.buffer;
5005
+ subscriber.buffer = null;
5006
+ if (buffered.length > 0) {
5007
+ ctx.connectionManager.sendToClient(client.id, {
5008
+ type: "view-changes",
5009
+ element: elementName,
5010
+ scope,
5011
+ changes: buffered
5012
+ });
5031
5013
  }
5032
- clientViewSubs.get(client.id).set(subscriptionId, cleanup);
5033
5014
  return true;
5034
5015
  }
5035
- if (message.type === "unsubscribe-query") {
5036
- const subs = clientViewSubs.get(client.id);
5037
- if (subs) {
5038
- const unsub = subs.get(message.subscriptionId);
5039
- if (unsub) {
5040
- unsub();
5041
- subs.delete(message.subscriptionId);
5042
- }
5043
- }
5016
+ if (message.type === "unsubscribe-view") {
5017
+ const scope = message.scope ?? "default";
5018
+ viewSubscribers.get(message.element)?.delete(`${client.id}:${scope}`);
5044
5019
  return true;
5045
5020
  }
5046
5021
  return false;
@@ -5052,7 +5027,7 @@ function arcWsHandlers() {
5052
5027
  syncEventsHandler(),
5053
5028
  requestSyncHandler(),
5054
5029
  executeCommandHandler(),
5055
- querySubscriptionHandler()
5030
+ viewSubscriptionHandler()
5056
5031
  ];
5057
5032
  }
5058
5033
  // src/create-server.ts
@@ -5065,6 +5040,7 @@ async function createArcServer(config) {
5065
5040
  const cronScheduler = new CronScheduler(contextHandler);
5066
5041
  cronScheduler.start();
5067
5042
  const connectionManager = new ConnectionManager;
5043
+ contextHandler.getEventPublisher().onViewChanges((changes) => broadcastViewChanges(changes, connectionManager));
5068
5044
  function buildCorsHeaders(req) {
5069
5045
  const origin = req?.headers.get("Origin") || "*";
5070
5046
  return {
@@ -5235,7 +5211,6 @@ async function createArcServer(config) {
5235
5211
  cronScheduler,
5236
5212
  stop: () => {
5237
5213
  cronScheduler.stop();
5238
- cleanupStreams();
5239
5214
  server.stop();
5240
5215
  }
5241
5216
  };
@@ -5248,12 +5223,11 @@ function hostLiveModel(context, dbAdapterFactory) {
5248
5223
  });
5249
5224
  }
5250
5225
  export {
5226
+ viewSubscriptionHandler,
5251
5227
  syncEventsHandler,
5252
- streamHandler,
5253
5228
  scopeAuthHandler,
5254
5229
  routeHandler,
5255
5230
  requestSyncHandler,
5256
- querySubscriptionHandler,
5257
5231
  queryHandler,
5258
5232
  hostLiveModel,
5259
5233
  healthHandler,
@@ -5263,10 +5237,10 @@ export {
5263
5237
  eventSyncHandler,
5264
5238
  createArcServer,
5265
5239
  commandHandler,
5266
- cleanupStreams,
5267
5240
  cleanupClientSubs,
5268
5241
  canTokenReceiveEvent,
5269
5242
  canTokenEmitEvent,
5243
+ broadcastViewChanges,
5270
5244
  arcWsHandlers,
5271
5245
  arcHttpHandlers,
5272
5246
  CronScheduler,
@@ -5274,5 +5248,5 @@ export {
5274
5248
  ConnectionManager
5275
5249
  };
5276
5250
 
5277
- //# debugId=D9115398B339A37F64756E2164756E21
5251
+ //# debugId=094B6B350E52DB8A64756E2164756E21
5278
5252
  //# sourceMappingURL=index.js.map