orez-lite 0.10.7 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/browser/host.d.ts.map +1 -1
  2. package/dist/browser/host.js +8 -29
  3. package/dist/browser/host.js.map +1 -1
  4. package/dist/browser/types.d.ts +4 -2
  5. package/dist/browser/types.d.ts.map +1 -1
  6. package/dist/browser/types.js.map +1 -1
  7. package/dist/cf-do/application-sql.d.ts +24 -5
  8. package/dist/cf-do/application-sql.d.ts.map +1 -1
  9. package/dist/cf-do/application-sql.js +51 -43
  10. package/dist/cf-do/application-sql.js.map +1 -1
  11. package/dist/cf-do/worker.d.ts +74 -14
  12. package/dist/cf-do/worker.d.ts.map +1 -1
  13. package/dist/cf-do/worker.js +195 -92
  14. package/dist/cf-do/worker.js.map +1 -1
  15. package/dist/do-sql-tracking.d.ts +17 -0
  16. package/dist/do-sql-tracking.d.ts.map +1 -1
  17. package/dist/do-sql-tracking.js +17 -0
  18. package/dist/do-sql-tracking.js.map +1 -1
  19. package/dist/realtime/host.d.ts +7 -0
  20. package/dist/realtime/host.d.ts.map +1 -0
  21. package/dist/realtime/host.js +77 -0
  22. package/dist/realtime/host.js.map +1 -0
  23. package/dist/realtime/hub.d.ts +10 -5
  24. package/dist/realtime/hub.d.ts.map +1 -1
  25. package/dist/realtime/hub.js +133 -53
  26. package/dist/realtime/hub.js.map +1 -1
  27. package/dist/realtime/index.d.ts +15 -2
  28. package/dist/realtime/index.d.ts.map +1 -1
  29. package/dist/realtime/index.js +12 -0
  30. package/dist/realtime/index.js.map +1 -1
  31. package/dist/realtime/local.d.ts +15 -0
  32. package/dist/realtime/local.d.ts.map +1 -0
  33. package/dist/realtime/local.js +63 -0
  34. package/dist/realtime/local.js.map +1 -0
  35. package/dist/realtime/manifest.d.ts +5 -1
  36. package/dist/realtime/manifest.d.ts.map +1 -1
  37. package/dist/realtime/manifest.js +1 -1
  38. package/dist/realtime/manifest.js.map +1 -1
  39. package/dist/realtime/message-port.d.ts +34 -0
  40. package/dist/realtime/message-port.d.ts.map +1 -0
  41. package/dist/realtime/message-port.js +141 -0
  42. package/dist/realtime/message-port.js.map +1 -0
  43. package/dist/realtime/producer-socket.d.ts +10 -0
  44. package/dist/realtime/producer-socket.d.ts.map +1 -0
  45. package/dist/realtime/producer-socket.js +90 -0
  46. package/dist/realtime/producer-socket.js.map +1 -0
  47. package/dist/realtime/producer.d.ts +16 -0
  48. package/dist/realtime/producer.d.ts.map +1 -0
  49. package/dist/realtime/producer.js +51 -0
  50. package/dist/realtime/producer.js.map +1 -0
  51. package/dist/realtime/protocol.d.ts +38 -2
  52. package/dist/realtime/protocol.d.ts.map +1 -1
  53. package/dist/realtime/protocol.js +34 -0
  54. package/dist/realtime/protocol.js.map +1 -1
  55. package/dist/realtime/publisher.d.ts +1 -0
  56. package/dist/realtime/publisher.d.ts.map +1 -1
  57. package/dist/realtime/publisher.js +29 -1
  58. package/dist/realtime/publisher.js.map +1 -1
  59. package/dist/realtime/socket-host.d.ts +25 -0
  60. package/dist/realtime/socket-host.d.ts.map +1 -0
  61. package/dist/realtime/socket-host.js +95 -0
  62. package/dist/realtime/socket-host.js.map +1 -0
  63. package/dist/realtime/store.d.ts +4 -4
  64. package/dist/realtime/store.d.ts.map +1 -1
  65. package/dist/realtime/store.js +0 -0
  66. package/dist/realtime/store.js.map +1 -1
  67. package/dist/realtime/writer.d.ts +17 -0
  68. package/dist/realtime/writer.d.ts.map +1 -0
  69. package/dist/realtime/writer.js +138 -0
  70. package/dist/realtime/writer.js.map +1 -0
  71. package/package.json +4 -4
@@ -97,15 +97,17 @@ const APPLICATION_SQL_DISPOSE = Symbol('applicationSqlDispose');
97
97
  class ApplicationSqlSessionTarget extends RpcTarget {
98
98
  owner;
99
99
  sessionID;
100
+ readOnly;
100
101
  state = 'created';
101
102
  changed = false;
102
103
  mutated = false;
103
- constructor(owner, sessionID) {
104
+ constructor(owner, sessionID, readOnly) {
104
105
  super();
105
106
  this.owner = owner;
106
107
  this.sessionID = sessionID;
108
+ this.readOnly = readOnly;
107
109
  }
108
- async begin() {
110
+ begin() {
109
111
  return this.owner[APPLICATION_SQL_ACQUIRE](this);
110
112
  }
111
113
  query(sql, params = []) {
@@ -142,7 +144,9 @@ export class ZeroDO extends DurableObject {
142
144
  activeWriteMeasurements = null;
143
145
  writeBudgetTripStatement;
144
146
  pendingChangesSchemaReady = false;
145
- activeApplicationSqlSession = null;
147
+ applicationSqlWriter = null;
148
+ applicationSqlReaders = new Set();
149
+ applicationSqlQueue = [];
146
150
  applicationSqlDidCommit(_changed, _mutated) { }
147
151
  recordWriteBudgetRows(rows, statement) {
148
152
  const wasTripped = this.writeBudget.status().tripped;
@@ -990,70 +994,150 @@ export class ZeroDO extends DurableObject {
990
994
  }));
991
995
  }
992
996
  assertApplicationSqlSession(session) {
993
- if (session.state !== 'active' || this.activeApplicationSqlSession !== session) {
997
+ const admitted = session.readOnly
998
+ ? this.applicationSqlReaders.has(session)
999
+ : this.applicationSqlWriter === session;
1000
+ if (session.state !== 'active' || !admitted) {
994
1001
  throw new Error('application SQLite session is not active');
995
1002
  }
996
1003
  }
997
- [APPLICATION_SQL_ACQUIRE](session) {
998
- if (session.state === 'active' && this.activeApplicationSqlSession === session) {
999
- return true;
1004
+ /**
1005
+ * Read sessions never mutate, so no lane can escalate and there is nothing to
1006
+ * deadlock over: a reader waits only on the writer, and the writer waits only
1007
+ * on the reader set draining.
1008
+ */
1009
+ canAdmitApplicationSqlSession(session) {
1010
+ if (this.applicationSqlWriter)
1011
+ return false;
1012
+ return session.readOnly || this.applicationSqlReaders.size === 0;
1013
+ }
1014
+ /**
1015
+ * Admit from the head of the arrival queue and stop at the first waiter that
1016
+ * cannot run yet.
1017
+ *
1018
+ * The stop is what makes this fair. Scanning past a blocked writer to admit
1019
+ * younger readers would let a steady read load hold the turn away from it
1020
+ * indefinitely, which is the starvation the competitive `begin()` poll this
1021
+ * replaces produced in production: callers raced a 25 ms timer for one
1022
+ * ownership flag, so admission order was decided by poll phase, and sessions
1023
+ * that had arrived ten seconds earlier watched later ones complete.
1024
+ */
1025
+ pumpApplicationSqlQueue() {
1026
+ while (this.applicationSqlQueue.length > 0) {
1027
+ const waiter = this.applicationSqlQueue[0];
1028
+ if (waiter.session.state !== 'waiting') {
1029
+ this.applicationSqlQueue.shift();
1030
+ clearTimeout(waiter.timer);
1031
+ continue;
1032
+ }
1033
+ if (!this.canAdmitApplicationSqlSession(waiter.session))
1034
+ return;
1035
+ this.applicationSqlQueue.shift();
1036
+ clearTimeout(waiter.timer);
1037
+ waiter.session.state = 'active';
1038
+ if (waiter.session.readOnly)
1039
+ this.applicationSqlReaders.add(waiter.session);
1040
+ else
1041
+ this.applicationSqlWriter = waiter.session;
1042
+ waiter.admit();
1000
1043
  }
1044
+ }
1045
+ /**
1046
+ * Join the arrival queue and resolve when this session owns its turn.
1047
+ *
1048
+ * A waiting session holds one queue entry and one timer, and nothing else:
1049
+ * cancellation, rollback and RPC stub disposal all route through
1050
+ * `releaseApplicationSqlTurn`, which drops the entry and re-pumps. The
1051
+ * deadline bounds a queue entry whose caller neither cancels nor disposes,
1052
+ * and reports the wait as an error instead of hanging the request.
1053
+ */
1054
+ [APPLICATION_SQL_ACQUIRE](session) {
1001
1055
  if (session.state !== 'created') {
1002
- throw new Error('application SQLite session cannot begin again');
1003
- }
1004
- if (this.activeApplicationSqlSession)
1005
- return false;
1006
- session.state = 'active';
1007
- this.activeApplicationSqlSession = session;
1008
- return true;
1056
+ return Promise.reject(new Error('application SQLite session cannot begin again'));
1057
+ }
1058
+ session.state = 'waiting';
1059
+ const admission = new Promise((resolve, reject) => {
1060
+ const waiter = {
1061
+ session,
1062
+ admit: resolve,
1063
+ reject,
1064
+ timer: setTimeout(() => {
1065
+ const index = this.applicationSqlQueue.indexOf(waiter);
1066
+ if (index < 0)
1067
+ return;
1068
+ this.applicationSqlQueue.splice(index, 1);
1069
+ session.state = 'closed';
1070
+ reject(new Error('timed out acquiring the application SQLite session'));
1071
+ }, APPLICATION_SQL_TURN_WAIT_MS),
1072
+ };
1073
+ this.applicationSqlQueue.push(waiter);
1074
+ this.pumpApplicationSqlQueue();
1075
+ });
1076
+ return admission;
1009
1077
  }
1078
+ /**
1079
+ * Close a session in any state and hand the turn to the next arrival.
1080
+ *
1081
+ * A session that is still queued has never touched SQLite, so dropping its
1082
+ * queue entry is the whole of its cleanup.
1083
+ */
1010
1084
  releaseApplicationSqlTurn(session) {
1085
+ if (session.state === 'closed')
1086
+ return;
1087
+ if (session.state === 'created') {
1088
+ session.state = 'closed';
1089
+ return;
1090
+ }
1091
+ if (session.state === 'waiting') {
1092
+ const index = this.applicationSqlQueue.findIndex((waiter) => waiter.session === session);
1093
+ if (index >= 0) {
1094
+ const [waiter] = this.applicationSqlQueue.splice(index, 1);
1095
+ clearTimeout(waiter.timer);
1096
+ }
1097
+ // The in-flight `begin()` is left unsettled on purpose. Only this
1098
+ // session's own caller can close it while it waits, and by then that
1099
+ // caller has stopped awaiting admission and is on its way to disposing
1100
+ // the stub, which cancels the call. Rejecting instead delivers an error
1101
+ // nobody is listening for, and workerd reports every one of those as an
1102
+ // uncaught error — measured, one per cancellation.
1103
+ session.state = 'closed';
1104
+ return;
1105
+ }
1011
1106
  this.assertApplicationSqlSession(session);
1012
1107
  session.state = 'closed';
1013
- this.activeApplicationSqlSession = null;
1108
+ if (session.readOnly)
1109
+ this.applicationSqlReaders.delete(session);
1110
+ else
1111
+ this.applicationSqlWriter = null;
1112
+ this.pumpApplicationSqlQueue();
1014
1113
  }
1015
1114
  async withApplicationSqlTurn(work, signal) {
1016
- const session = await this.applicationSqlSession(crypto.randomUUID());
1115
+ const session = await this.applicationSqlSession(crypto.randomUUID(), {
1116
+ readOnly: true,
1117
+ });
1017
1118
  const dispose = () => session[Symbol.dispose]();
1018
1119
  signal?.addEventListener('abort', dispose, { once: true });
1019
1120
  try {
1020
- while (!(await session.begin())) {
1021
- if (signal?.aborted)
1022
- throw new Error('application SQLite request was canceled');
1023
- await new Promise((resolve) => setTimeout(resolve, 25));
1024
- }
1121
+ await session.begin();
1025
1122
  return await work();
1026
1123
  }
1027
1124
  finally {
1028
1125
  signal?.removeEventListener('abort', dispose);
1029
- if (session.state === 'active')
1030
- this.releaseApplicationSqlTurn(session);
1031
- else if (session.state !== 'closed')
1032
- dispose();
1126
+ dispose();
1033
1127
  }
1034
1128
  }
1035
1129
  /**
1036
1130
  * drive one application SQLite session from INSIDE this durable object.
1037
1131
  *
1038
1132
  * identical to the client-side session helper in application-sql.ts — same
1039
- * ownership acquisition, same per-statement journal, same commit/rollback —
1040
- * except the begin poll and every statement are local calls instead of RPCs.
1041
- * the caller must already be running in this object.
1133
+ * admission, same per-statement journal, same commit/rollback — except every
1134
+ * call is local instead of an RPC. the caller must already be running in this
1135
+ * object.
1042
1136
  */
1043
- async withLocalApplicationSqlSession(work) {
1044
- const session = await this.applicationSqlSession(crypto.randomUUID());
1137
+ async withLocalApplicationSqlSession(readOnly, work) {
1138
+ const session = await this.applicationSqlSession(crypto.randomUUID(), { readOnly });
1045
1139
  try {
1046
- // bounded, unlike the client-side poll. a remote caller that goes away
1047
- // stops polling and frees the turn; a local caller has no signal to
1048
- // observe, so an unbounded loop would spin for the life of the object
1049
- // every time somebody abandoned a request mid-reconcile.
1050
- const deadline = Date.now() + APPLICATION_SQL_TURN_WAIT_MS;
1051
- while (!(await session.begin())) {
1052
- if (Date.now() >= deadline) {
1053
- throw new Error('timed out acquiring the application SQLite session');
1054
- }
1055
- await scheduler.wait(25);
1056
- }
1140
+ await session.begin();
1057
1141
  const value = await work(session);
1058
1142
  await session.commit();
1059
1143
  return value;
@@ -1079,20 +1163,22 @@ export class ZeroDO extends DurableObject {
1079
1163
  * the transport is.
1080
1164
  */
1081
1165
  applicationSqlLocalClient(namespace) {
1082
- const run = (work) => this.withLocalApplicationSqlSession(work);
1166
+ const run = (readOnly, work) => this.withLocalApplicationSqlSession(readOnly, work);
1167
+ const transaction = (readOnly, compileQuery, work, queryBudget) => run(readOnly, async (session) => work({
1168
+ exec: (sql, params = [], metadata) => session.exec(sql, params, metadata),
1169
+ query: (sql, params = []) => session.query(sql, params),
1170
+ registerTables: (tables) => session.registerTables(tables),
1171
+ async queryAst(ast, format, queryName) {
1172
+ return session.queryPlan(await compileQuery(ast, format), queryName, queryBudget);
1173
+ },
1174
+ }));
1083
1175
  return {
1084
1176
  namespace,
1085
- query: (sql, params = []) => run((session) => session.query(sql, params)),
1086
- exec: (sql, params = [], metadata) => run((session) => session.exec(sql, params, metadata)),
1087
- registerTables: (tables) => run((session) => session.registerTables(tables)),
1088
- transaction: (compileQuery, work, queryBudget) => run(async (session) => work({
1089
- exec: (sql, params = [], metadata) => session.exec(sql, params, metadata),
1090
- query: (sql, params = []) => session.query(sql, params),
1091
- registerTables: (tables) => session.registerTables(tables),
1092
- async queryAst(ast, format, queryName) {
1093
- return session.queryPlan(await compileQuery(ast, format), queryName, queryBudget);
1094
- },
1095
- })),
1177
+ query: (sql, params = []) => run(true, (session) => session.query(sql, params)),
1178
+ exec: (sql, params = [], metadata) => run(false, (session) => session.exec(sql, params, metadata)),
1179
+ registerTables: (tables) => run(false, (session) => session.registerTables(tables)),
1180
+ transaction: (compileQuery, work, queryBudget) => transaction(false, compileQuery, work, queryBudget),
1181
+ readTransaction: (compileQuery, work, queryBudget) => transaction(true, compileQuery, work, queryBudget),
1096
1182
  };
1097
1183
  }
1098
1184
  registerApplicationSqlTables(tables) {
@@ -1106,14 +1192,25 @@ export class ZeroDO extends DurableObject {
1106
1192
  }
1107
1193
  /**
1108
1194
  * private durable object RPC surface for the application SQLite client. the
1109
- * disposable target exists before ownership acquisition, so waiting callers
1110
- * hold no server state and active cancellation rolls back the transaction.
1111
- * this method is intentionally absent from fetch().
1195
+ * disposable target exists before admission, so a queued caller can cancel by
1196
+ * disposing it and an active one rolls its transaction back. this method is
1197
+ * intentionally absent from fetch().
1112
1198
  */
1113
- async applicationSqlSession(sessionID) {
1199
+ async applicationSqlSession(sessionID, options = {}) {
1114
1200
  if (!sessionID)
1115
1201
  throw new TypeError('application SQLite session id is required');
1116
- return new ApplicationSqlSessionTarget(this, sessionID);
1202
+ return new ApplicationSqlSessionTarget(this, sessionID, options.readOnly === true);
1203
+ }
1204
+ /**
1205
+ * Run one read statement end to end in a single round trip.
1206
+ *
1207
+ * A single statement is already atomic, so a session that exists only to
1208
+ * carry it costs four sequential RPCs (open, admit, run, commit) for one
1209
+ * SELECT — the shape every authenticated read on the sync path pays. This
1210
+ * opens, admits, runs and closes the same read session locally instead.
1211
+ */
1212
+ async applicationSqlQuery(sql, params = []) {
1213
+ return this.withLocalApplicationSqlSession(true, (session) => session.query(sql, params));
1117
1214
  }
1118
1215
  prepareApplicationSqlMutation(sessionID, sql) {
1119
1216
  if (!isSqlMutation(sql))
@@ -1132,8 +1229,20 @@ export class ZeroDO extends DurableObject {
1132
1229
  beginTxJournal(this.sql, sessionID, 'application');
1133
1230
  return true;
1134
1231
  }
1135
- async [APPLICATION_SQL_QUERY](session, sql, params = []) {
1232
+ /**
1233
+ * A read session declared its lane before it was admitted, and readers run
1234
+ * alongside each other on the strength of that declaration. Escalating one
1235
+ * mid-session would strand every other reader on a state that is no longer
1236
+ * committed, so a mutation here is an error rather than an upgrade.
1237
+ */
1238
+ assertApplicationSqlStatement(session, sql) {
1136
1239
  this.assertApplicationSqlSession(session);
1240
+ if (session.readOnly && classifySql(sql).mutation) {
1241
+ throw new Error('read-only application SQLite session cannot execute a mutation');
1242
+ }
1243
+ }
1244
+ async [APPLICATION_SQL_QUERY](session, sql, params = []) {
1245
+ this.assertApplicationSqlStatement(session, sql);
1137
1246
  return this.atomically(() => {
1138
1247
  if (this.prepareApplicationSqlMutation(session.sessionID, sql)) {
1139
1248
  session.mutated = true;
@@ -1142,7 +1251,7 @@ export class ZeroDO extends DurableObject {
1142
1251
  });
1143
1252
  }
1144
1253
  async [APPLICATION_SQL_EXEC](session, sql, params = [], metadata) {
1145
- this.assertApplicationSqlSession(session);
1254
+ this.assertApplicationSqlStatement(session, sql);
1146
1255
  return this.atomically(() => {
1147
1256
  if (this.prepareApplicationSqlMutation(session.sessionID, sql)) {
1148
1257
  session.mutated = true;
@@ -1155,10 +1264,16 @@ export class ZeroDO extends DurableObject {
1155
1264
  }
1156
1265
  async [APPLICATION_SQL_QUERY_PLAN](session, plan, queryName, queryBudget) {
1157
1266
  this.assertApplicationSqlSession(session);
1158
- return this.atomically(() => executeTransactionQueryPlan(plan, (sql, params) => this.executeSQL(sql, params, undefined, session.sessionID).rows, { queryName, budget: queryBudget }));
1267
+ return this.atomically(() => executeTransactionQueryPlan(plan, (sql, params) => {
1268
+ this.assertApplicationSqlStatement(session, sql);
1269
+ return this.executeSQL(sql, params, undefined, session.sessionID).rows;
1270
+ }, { queryName, budget: queryBudget }));
1159
1271
  }
1160
1272
  async [APPLICATION_SQL_REGISTER_TABLES](session, tables) {
1161
1273
  this.assertApplicationSqlSession(session);
1274
+ if (session.readOnly) {
1275
+ throw new Error('read-only application SQLite session cannot register tables');
1276
+ }
1162
1277
  await this.atomically(() => this.registerApplicationSqlTables(tables));
1163
1278
  }
1164
1279
  async [APPLICATION_SQL_COMMIT](session) {
@@ -1176,14 +1291,16 @@ export class ZeroDO extends DurableObject {
1176
1291
  }
1177
1292
  this.applicationSqlDidCommit(session.changed, session.mutated);
1178
1293
  }
1179
- async [APPLICATION_SQL_ROLLBACK](session) {
1180
- if (session.state === 'created') {
1181
- session.state = 'closed';
1182
- return;
1183
- }
1184
- this.assertApplicationSqlSession(session);
1185
- try {
1186
- if (session.mutated) {
1294
+ /**
1295
+ * Undo whatever this session wrote and hand its turn on.
1296
+ *
1297
+ * Rollback, cancellation and RPC stub disposal are the same operation: a
1298
+ * session that never reached SQLite only has to leave the queue, and one that
1299
+ * did has to replay its row images before releasing.
1300
+ */
1301
+ closeApplicationSqlSession(session) {
1302
+ if (session.state === 'active' && session.mutated) {
1303
+ try {
1187
1304
  this.rollbackAtomicallyWithoutForeignKeys(() => {
1188
1305
  this.rollbackPendingTrackedChanges(session.sessionID);
1189
1306
  rollbackTxJournal(this.sql, session.sessionID);
@@ -1191,32 +1308,18 @@ export class ZeroDO extends DurableObject {
1191
1308
  });
1192
1309
  this.invalidateSchemaCaches();
1193
1310
  }
1311
+ finally {
1312
+ this.releaseApplicationSqlTurn(session);
1313
+ }
1314
+ return;
1194
1315
  }
1195
- finally {
1196
- this.releaseApplicationSqlTurn(session);
1197
- }
1316
+ this.releaseApplicationSqlTurn(session);
1317
+ }
1318
+ async [APPLICATION_SQL_ROLLBACK](session) {
1319
+ this.closeApplicationSqlSession(session);
1198
1320
  }
1199
1321
  [APPLICATION_SQL_DISPOSE](session) {
1200
- if (session.state === 'closed')
1201
- return;
1202
- if (session.state === 'created') {
1203
- session.state = 'closed';
1204
- return;
1205
- }
1206
- this.assertApplicationSqlSession(session);
1207
- try {
1208
- if (session.mutated) {
1209
- this.rollbackAtomicallyWithoutForeignKeys(() => {
1210
- this.rollbackPendingTrackedChanges(session.sessionID);
1211
- rollbackTxJournal(this.sql, session.sessionID);
1212
- this.deletePendingTrackedChanges(session.sessionID);
1213
- });
1214
- this.invalidateSchemaCaches();
1215
- }
1216
- }
1217
- finally {
1218
- this.releaseApplicationSqlTurn(session);
1219
- }
1322
+ this.closeApplicationSqlSession(session);
1220
1323
  }
1221
1324
  atomicallySync(work) {
1222
1325
  try {