@aurabase/aurabase-js 0.3.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.
- package/.changeset/config.json +11 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +833 -0
- package/package.json +90 -0
- package/packages/ai/LICENSE +21 -0
- package/packages/ai/README.md +106 -0
- package/packages/ai/dist/index.cjs +229 -0
- package/packages/ai/dist/index.d.cts +139 -0
- package/packages/ai/dist/index.d.ts +139 -0
- package/packages/ai/dist/index.js +226 -0
- package/packages/ai/node_modules/.bin/esbuild +19 -0
- package/packages/ai/node_modules/.bin/tsc +22 -0
- package/packages/ai/node_modules/.bin/tsserver +22 -0
- package/packages/ai/node_modules/.bin/tsup +22 -0
- package/packages/ai/node_modules/.bin/tsup-node +22 -0
- package/packages/ai/node_modules/.bin/vite +22 -0
- package/packages/ai/node_modules/.bin/vitest +22 -0
- package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/ai/package.json +61 -0
- package/packages/ai/src/AiService.ts +226 -0
- package/packages/ai/src/index.ts +18 -0
- package/packages/ai/src/sse.ts +141 -0
- package/packages/ai/src/types.ts +30 -0
- package/packages/ai/tests/chat-stream.test.ts +151 -0
- package/packages/ai/tests/rag-namespaces.test.ts +54 -0
- package/packages/ai/tests/sse.test.ts +69 -0
- package/packages/ai/tsconfig.json +5 -0
- package/packages/ai/tsup.config.ts +11 -0
- package/packages/aurabase/LICENSE +21 -0
- package/packages/aurabase/README.md +64 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts +77 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts +77 -0
- package/packages/aurabase/dist/index.cjs +237 -0
- package/packages/aurabase/dist/index.d.cts +46 -0
- package/packages/aurabase/dist/index.d.ts +46 -0
- package/packages/aurabase/dist/index.js +177 -0
- package/packages/aurabase/dist/ssr.cjs +224 -0
- package/packages/aurabase/dist/ssr.d.cts +65 -0
- package/packages/aurabase/dist/ssr.d.ts +65 -0
- package/packages/aurabase/dist/ssr.js +214 -0
- package/packages/aurabase/node_modules/.bin/esbuild +19 -0
- package/packages/aurabase/node_modules/.bin/tsc +22 -0
- package/packages/aurabase/node_modules/.bin/tsserver +22 -0
- package/packages/aurabase/node_modules/.bin/tsup +22 -0
- package/packages/aurabase/node_modules/.bin/tsup-node +22 -0
- package/packages/aurabase/node_modules/.bin/vite +22 -0
- package/packages/aurabase/node_modules/.bin/vitest +22 -0
- package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/aurabase/package.json +80 -0
- package/packages/aurabase/src/AuraClient.ts +237 -0
- package/packages/aurabase/src/index.ts +152 -0
- package/packages/aurabase/src/ssr.ts +156 -0
- package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +82 -0
- package/packages/aurabase/tests/ai.test.ts +111 -0
- package/packages/aurabase/tests/auth-admin.test.ts +515 -0
- package/packages/aurabase/tests/auth-e2e.test.ts +135 -0
- package/packages/aurabase/tests/auth-hardening.test.ts +206 -0
- package/packages/aurabase/tests/auth.test.ts +947 -0
- package/packages/aurabase/tests/database.test.ts +230 -0
- package/packages/aurabase/tests/db-types-e2e.test.ts +217 -0
- package/packages/aurabase/tests/dispose.test.ts +23 -0
- package/packages/aurabase/tests/foreign-token.e2e.test.ts +129 -0
- package/packages/aurabase/tests/functions.test.ts +107 -0
- package/packages/aurabase/tests/integration.test.ts +413 -0
- package/packages/aurabase/tests/live-production-validation.js +241 -0
- package/packages/aurabase/tests/live-sdk-production-validation.js +293 -0
- package/packages/aurabase/tests/mocks.ts +139 -0
- package/packages/aurabase/tests/notifications.test.ts +314 -0
- package/packages/aurabase/tests/observability-backup.test.ts +186 -0
- package/packages/aurabase/tests/observability-hook.test.ts +130 -0
- package/packages/aurabase/tests/production-robustness.test.ts +268 -0
- package/packages/aurabase/tests/public-api-surface.test.ts +53 -0
- package/packages/aurabase/tests/query-builder.test.ts +329 -0
- package/packages/aurabase/tests/readme-smoke-e2e.test.ts +231 -0
- package/packages/aurabase/tests/realtime-filter-e2e.test.ts +344 -0
- package/packages/aurabase/tests/refresh-locks.test.ts +210 -0
- package/packages/aurabase/tests/retry-option.test.ts +17 -0
- package/packages/aurabase/tests/rpc-e2e.test.ts +254 -0
- package/packages/aurabase/tests/rs256-local-e2e.test.ts +114 -0
- package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +235 -0
- package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +124 -0
- package/packages/aurabase/tests/service-role-guard.test.ts +99 -0
- package/packages/aurabase/tests/ssr.test.ts +91 -0
- package/packages/aurabase/tests/storage-backend-e2e.test.ts +481 -0
- package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +104 -0
- package/packages/aurabase/tests/storage-transform-e2e.test.ts +684 -0
- package/packages/aurabase/tests/storage.test.ts +55 -0
- package/packages/aurabase/tests/types/client-generic.test-d.ts +79 -0
- package/packages/aurabase/tests/types/docs-snippets.test-d.ts +571 -0
- package/packages/aurabase/tests/types/error-code.test-d.ts +53 -0
- package/packages/aurabase/tsconfig.json +5 -0
- package/packages/aurabase/tsconfig.test-d.json +5 -0
- package/packages/aurabase/tsup.config.ts +11 -0
- package/packages/aurabase/vitest.config.ts +16 -0
- package/packages/aurabase/vitest.e2e.config.ts +9 -0
- package/packages/auth/LICENSE +21 -0
- package/packages/auth/README.md +215 -0
- package/packages/auth/dist/index.cjs +1175 -0
- package/packages/auth/dist/index.d.cts +397 -0
- package/packages/auth/dist/index.d.ts +397 -0
- package/packages/auth/dist/index.js +1170 -0
- package/packages/auth/node_modules/.bin/esbuild +19 -0
- package/packages/auth/node_modules/.bin/tsc +22 -0
- package/packages/auth/node_modules/.bin/tsserver +22 -0
- package/packages/auth/node_modules/.bin/tsup +22 -0
- package/packages/auth/node_modules/.bin/tsup-node +22 -0
- package/packages/auth/node_modules/.bin/vite +22 -0
- package/packages/auth/node_modules/.bin/vitest +22 -0
- package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/auth/package.json +61 -0
- package/packages/auth/src/AuthAdminService.ts +149 -0
- package/packages/auth/src/AuthService.ts +1166 -0
- package/packages/auth/src/SessionManager.ts +92 -0
- package/packages/auth/src/index.ts +17 -0
- package/packages/auth/src/jwks.ts +337 -0
- package/packages/auth/src/pkce.ts +27 -0
- package/packages/auth/src/types.ts +89 -0
- package/packages/auth/tests/auth-lifecycle.test.ts +149 -0
- package/packages/auth/tests/detect-session.test.ts +361 -0
- package/packages/auth/tests/getclaims.test.ts +780 -0
- package/packages/auth/tests/is-mfa-challenge.test.ts +27 -0
- package/packages/auth/tests/password-recovery.test.ts +124 -0
- package/packages/auth/tests/pkce.test.ts +192 -0
- package/packages/auth/tests/session-manager.test.ts +103 -0
- package/packages/auth/tests/set-session.test.ts +139 -0
- package/packages/auth/tests/verify-otp.test.ts +153 -0
- package/packages/auth/tsconfig.json +5 -0
- package/packages/auth/tsup.config.ts +11 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +132 -0
- package/packages/core/dist/index.cjs +695 -0
- package/packages/core/dist/index.d.cts +1137 -0
- package/packages/core/dist/index.d.ts +1137 -0
- package/packages/core/dist/index.js +673 -0
- package/packages/core/node_modules/.bin/esbuild +22 -0
- package/packages/core/node_modules/.bin/tsc +22 -0
- package/packages/core/node_modules/.bin/tsserver +22 -0
- package/packages/core/node_modules/.bin/tsup +22 -0
- package/packages/core/node_modules/.bin/tsup-node +22 -0
- package/packages/core/node_modules/.bin/vite +22 -0
- package/packages/core/node_modules/.bin/vitest +22 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +57 -0
- package/packages/core/src/AuraError.ts +89 -0
- package/packages/core/src/BaseService.ts +44 -0
- package/packages/core/src/client.ts +34 -0
- package/packages/core/src/cookie-utils.ts +42 -0
- package/packages/core/src/fetch.ts +435 -0
- package/packages/core/src/index.ts +22 -0
- package/packages/core/src/retry.ts +63 -0
- package/packages/core/src/serialize.ts +6 -0
- package/packages/core/src/stores/BaseAuthStore.ts +164 -0
- package/packages/core/src/stores/LocalAuthStore.ts +199 -0
- package/packages/core/src/types.ts +1042 -0
- package/packages/core/tests/auth-store.test.ts +300 -0
- package/packages/core/tests/error-code.test.ts +61 -0
- package/packages/core/tests/error.test.ts +24 -0
- package/packages/core/tests/fetch.test.ts +286 -0
- package/packages/core/tests/mocks.ts +139 -0
- package/packages/core/tests/observability.test.ts +86 -0
- package/packages/core/tests/refresh-hardening.test.ts +164 -0
- package/packages/core/tests/refresh-observability.test.ts +111 -0
- package/packages/core/tests/retry.test.ts +106 -0
- package/packages/core/tests/serialize.test.ts +12 -0
- package/packages/core/tests/timeout.test.ts +79 -0
- package/packages/core/tsconfig.json +5 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/db/LICENSE +21 -0
- package/packages/db/README.md +152 -0
- package/packages/db/VENDORED.md +93 -0
- package/packages/db/dist/index.cjs +544 -0
- package/packages/db/dist/index.d.cts +1111 -0
- package/packages/db/dist/index.d.ts +1111 -0
- package/packages/db/dist/index.js +541 -0
- package/packages/db/node_modules/.bin/esbuild +19 -0
- package/packages/db/node_modules/.bin/tsc +22 -0
- package/packages/db/node_modules/.bin/tsserver +22 -0
- package/packages/db/node_modules/.bin/tsup +22 -0
- package/packages/db/node_modules/.bin/tsup-node +22 -0
- package/packages/db/node_modules/.bin/vite +22 -0
- package/packages/db/node_modules/.bin/vitest +22 -0
- package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/db/package.json +62 -0
- package/packages/db/src/DatabaseService.ts +170 -0
- package/packages/db/src/QueryBuilder.ts +830 -0
- package/packages/db/src/index.ts +2 -0
- package/packages/db/src/pgrst-types/common/common.ts +86 -0
- package/packages/db/src/pgrst-types/common/rpc.ts +151 -0
- package/packages/db/src/pgrst-types/feature-flags.ts +20 -0
- package/packages/db/src/pgrst-types/types.ts +134 -0
- package/packages/db/src/select-query-parser/parser.ts +479 -0
- package/packages/db/src/select-query-parser/result.ts +553 -0
- package/packages/db/src/select-query-parser/types.ts +132 -0
- package/packages/db/src/select-query-parser/utils.ts +707 -0
- package/packages/db/tests/filters.test.ts +81 -0
- package/packages/db/tests/mutations-filtered.e2e.test.ts +132 -0
- package/packages/db/tests/mutations-filtered.test.ts +188 -0
- package/packages/db/tests/order.test.ts +45 -0
- package/packages/db/tests/rpc.test.ts +41 -0
- package/packages/db/tests/scale/build-large-schema.ts +94 -0
- package/packages/db/tests/scale/scale-compile.test.ts +167 -0
- package/packages/db/tests/schema.test.ts +77 -0
- package/packages/db/tests/single-accept.e2e.test.ts +123 -0
- package/packages/db/tests/single.test.ts +211 -0
- package/packages/db/tests/types/filters.test-d.ts +52 -0
- package/packages/db/tests/types/fixtures.ts +40 -0
- package/packages/db/tests/types/from-views.test-d.ts +18 -0
- package/packages/db/tests/types/mutations.test-d.ts +23 -0
- package/packages/db/tests/types/rpc.test-d.ts +60 -0
- package/packages/db/tests/types/schema-shape.test-d.ts +8 -0
- package/packages/db/tests/types/select-inference.test-d.ts +38 -0
- package/packages/db/tests/vendored/fixtures.ts +83 -0
- package/packages/db/tests/vendored/get-result.test-d.ts +75 -0
- package/packages/db/tsconfig.json +5 -0
- package/packages/db/tsconfig.test-d.json +5 -0
- package/packages/db/tsup.config.ts +11 -0
- package/packages/db/vitest.config.ts +8 -0
- package/packages/functions/LICENSE +21 -0
- package/packages/functions/README.md +96 -0
- package/packages/functions/dist/index.cjs +173 -0
- package/packages/functions/dist/index.d.cts +81 -0
- package/packages/functions/dist/index.d.ts +81 -0
- package/packages/functions/dist/index.js +168 -0
- package/packages/functions/node_modules/.bin/esbuild +19 -0
- package/packages/functions/node_modules/.bin/tsc +22 -0
- package/packages/functions/node_modules/.bin/tsserver +22 -0
- package/packages/functions/node_modules/.bin/tsup +22 -0
- package/packages/functions/node_modules/.bin/tsup-node +22 -0
- package/packages/functions/node_modules/.bin/vite +22 -0
- package/packages/functions/node_modules/.bin/vitest +22 -0
- package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/functions/package.json +60 -0
- package/packages/functions/src/FunctionsService.ts +188 -0
- package/packages/functions/src/errors.ts +70 -0
- package/packages/functions/src/index.ts +6 -0
- package/packages/functions/tests/invoke.test.ts +97 -0
- package/packages/functions/tests/response-type-errors.test.ts +135 -0
- package/packages/functions/tsconfig.json +5 -0
- package/packages/functions/tsup.config.ts +11 -0
- package/packages/gen/LICENSE +21 -0
- package/packages/gen/README.md +84 -0
- package/packages/gen/dist/cli.cjs +334 -0
- package/packages/gen/dist/cli.d.cts +1 -0
- package/packages/gen/dist/cli.d.ts +1 -0
- package/packages/gen/dist/cli.js +332 -0
- package/packages/gen/dist/index.cjs +257 -0
- package/packages/gen/dist/index.d.cts +12 -0
- package/packages/gen/dist/index.d.ts +12 -0
- package/packages/gen/dist/index.js +255 -0
- package/packages/gen/node_modules/.bin/esbuild +19 -0
- package/packages/gen/node_modules/.bin/tsc +22 -0
- package/packages/gen/node_modules/.bin/tsserver +22 -0
- package/packages/gen/node_modules/.bin/tsup +22 -0
- package/packages/gen/node_modules/.bin/tsup-node +22 -0
- package/packages/gen/node_modules/.bin/vite +22 -0
- package/packages/gen/node_modules/.bin/vitest +22 -0
- package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/gen/package.json +66 -0
- package/packages/gen/src/cli.ts +67 -0
- package/packages/gen/src/generate.ts +0 -0
- package/packages/gen/src/index.ts +2 -0
- package/packages/gen/tests/compile.test.ts +213 -0
- package/packages/gen/tests/e2e.test.ts +130 -0
- package/packages/gen/tests/generate.test.ts +396 -0
- package/packages/gen/tsconfig.json +5 -0
- package/packages/gen/tsup.config.ts +11 -0
- package/packages/notifications/LICENSE +21 -0
- package/packages/notifications/README.md +134 -0
- package/packages/notifications/dist/index.cjs +128 -0
- package/packages/notifications/dist/index.d.cts +81 -0
- package/packages/notifications/dist/index.d.ts +81 -0
- package/packages/notifications/dist/index.js +125 -0
- package/packages/notifications/node_modules/.bin/esbuild +19 -0
- package/packages/notifications/node_modules/.bin/tsc +22 -0
- package/packages/notifications/node_modules/.bin/tsserver +22 -0
- package/packages/notifications/node_modules/.bin/tsup +22 -0
- package/packages/notifications/node_modules/.bin/tsup-node +22 -0
- package/packages/notifications/node_modules/.bin/vite +22 -0
- package/packages/notifications/node_modules/.bin/vitest +22 -0
- package/packages/notifications/package.json +60 -0
- package/packages/notifications/src/NotificationsService.ts +230 -0
- package/packages/notifications/src/index.ts +19 -0
- package/packages/notifications/tsconfig.json +5 -0
- package/packages/notifications/tsup.config.ts +11 -0
- package/packages/realtime/LICENSE +21 -0
- package/packages/realtime/README.md +183 -0
- package/packages/realtime/dist/index.cjs +977 -0
- package/packages/realtime/dist/index.d.cts +259 -0
- package/packages/realtime/dist/index.d.ts +259 -0
- package/packages/realtime/dist/index.js +973 -0
- package/packages/realtime/node_modules/.bin/esbuild +19 -0
- package/packages/realtime/node_modules/.bin/tsc +22 -0
- package/packages/realtime/node_modules/.bin/tsserver +22 -0
- package/packages/realtime/node_modules/.bin/tsup +22 -0
- package/packages/realtime/node_modules/.bin/tsup-node +22 -0
- package/packages/realtime/node_modules/.bin/vite +22 -0
- package/packages/realtime/node_modules/.bin/vitest +22 -0
- package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/realtime/package.json +60 -0
- package/packages/realtime/src/AuraChannel.ts +485 -0
- package/packages/realtime/src/HeartbeatManager.ts +73 -0
- package/packages/realtime/src/RealtimeService.ts +505 -0
- package/packages/realtime/src/columnFilter.ts +301 -0
- package/packages/realtime/src/index.ts +4 -0
- package/packages/realtime/tests/ack-routing.test.ts +81 -0
- package/packages/realtime/tests/column-filter.test.ts +144 -0
- package/packages/realtime/tests/dispatch.test.ts +191 -0
- package/packages/realtime/tests/heartbeat.test.ts +67 -0
- package/packages/realtime/tests/http-broadcast.test.ts +68 -0
- package/packages/realtime/tests/realtime-token.test.ts +43 -0
- package/packages/realtime/tests/reconnect-race.test.ts +149 -0
- package/packages/realtime/tests/ref-id.test.ts +34 -0
- package/packages/realtime/tests/set-auth.test.ts +70 -0
- package/packages/realtime/tsconfig.json +5 -0
- package/packages/realtime/tsup.config.ts +11 -0
- package/packages/storage/LICENSE +21 -0
- package/packages/storage/README.md +141 -0
- package/packages/storage/dist/index.cjs +333 -0
- package/packages/storage/dist/index.d.cts +185 -0
- package/packages/storage/dist/index.d.ts +185 -0
- package/packages/storage/dist/index.js +329 -0
- package/packages/storage/node_modules/.bin/esbuild +19 -0
- package/packages/storage/node_modules/.bin/tsc +22 -0
- package/packages/storage/node_modules/.bin/tsserver +22 -0
- package/packages/storage/node_modules/.bin/tsup +22 -0
- package/packages/storage/node_modules/.bin/tsup-node +22 -0
- package/packages/storage/node_modules/.bin/vite +22 -0
- package/packages/storage/node_modules/.bin/vitest +22 -0
- package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/storage/package.json +60 -0
- package/packages/storage/src/DownloadBuilder.ts +60 -0
- package/packages/storage/src/StorageService.ts +323 -0
- package/packages/storage/src/index.ts +2 -0
- package/packages/storage/src/path.ts +35 -0
- package/packages/storage/src/transform.ts +15 -0
- package/packages/storage/tests/download.test.ts +64 -0
- package/packages/storage/tests/parity.test.ts +154 -0
- package/packages/storage/tests/path.test.ts +87 -0
- package/packages/storage/tests/transform.test.ts +79 -0
- package/packages/storage/tsconfig.json +5 -0
- package/packages/storage/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +24 -0
- package/tsconfig.base.json +15 -0
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import { fetchWithAuth } from '@aurabase/core';
|
|
2
|
+
|
|
3
|
+
// src/RealtimeService.ts
|
|
4
|
+
|
|
5
|
+
// src/columnFilter.ts
|
|
6
|
+
var VALID_OPS = /* @__PURE__ */ new Set([
|
|
7
|
+
"eq",
|
|
8
|
+
"neq",
|
|
9
|
+
"lt",
|
|
10
|
+
"lte",
|
|
11
|
+
"gt",
|
|
12
|
+
"gte",
|
|
13
|
+
"in",
|
|
14
|
+
"like",
|
|
15
|
+
"ilike",
|
|
16
|
+
"is",
|
|
17
|
+
"match",
|
|
18
|
+
"imatch",
|
|
19
|
+
"isdistinct"
|
|
20
|
+
]);
|
|
21
|
+
var MAX_REGEX_PATTERN_LEN = 512;
|
|
22
|
+
var MAX_IN_VALUES = 100;
|
|
23
|
+
function parseColumnFilter(filterStr) {
|
|
24
|
+
const eqIdx = filterStr.indexOf("=");
|
|
25
|
+
if (eqIdx === -1) return null;
|
|
26
|
+
const column = filterStr.slice(0, eqIdx);
|
|
27
|
+
if (!column) return null;
|
|
28
|
+
let rest = filterStr.slice(eqIdx + 1);
|
|
29
|
+
let negated = false;
|
|
30
|
+
if (rest.startsWith("not.")) {
|
|
31
|
+
negated = true;
|
|
32
|
+
rest = rest.slice(4);
|
|
33
|
+
}
|
|
34
|
+
const dotIdx = rest.indexOf(".");
|
|
35
|
+
if (dotIdx === -1) return null;
|
|
36
|
+
const opStr = rest.slice(0, dotIdx);
|
|
37
|
+
let value = rest.slice(dotIdx + 1);
|
|
38
|
+
if (!VALID_OPS.has(opStr)) return null;
|
|
39
|
+
const op = opStr;
|
|
40
|
+
if (op === "in") {
|
|
41
|
+
const stripped = value.replace(/^\(+/, "").replace(/\)+$/, "");
|
|
42
|
+
if (stripped.split(",").length > MAX_IN_VALUES) return null;
|
|
43
|
+
value = stripped;
|
|
44
|
+
} else if (op === "is") {
|
|
45
|
+
if (value !== "null" && value !== "true" && value !== "false") return null;
|
|
46
|
+
} else if (op === "match" || op === "imatch") {
|
|
47
|
+
if (value.length > MAX_REGEX_PATTERN_LEN) return null;
|
|
48
|
+
try {
|
|
49
|
+
new RegExp(value, op === "imatch" ? "i" : void 0);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { column, op, value, negated };
|
|
55
|
+
}
|
|
56
|
+
function splitTopLevel(s) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
let depth = 0;
|
|
59
|
+
let start = 0;
|
|
60
|
+
for (let i = 0; i < s.length; i++) {
|
|
61
|
+
const c = s[i];
|
|
62
|
+
if (c === "(") depth += 1;
|
|
63
|
+
else if (c === ")") depth = Math.max(0, depth - 1);
|
|
64
|
+
else if (c === "," && depth === 0) {
|
|
65
|
+
parts.push(s.slice(start, i));
|
|
66
|
+
start = i + 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
parts.push(s.slice(start));
|
|
70
|
+
return parts;
|
|
71
|
+
}
|
|
72
|
+
function parseFilterSet(s) {
|
|
73
|
+
const parts = splitTopLevel(s);
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const part of parts) {
|
|
76
|
+
const cf = parseColumnFilter(part.trim());
|
|
77
|
+
if (!cf) return null;
|
|
78
|
+
out.push(cf);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function jsonValueToString(field) {
|
|
83
|
+
if (typeof field === "string") return field;
|
|
84
|
+
if (typeof field === "number" || typeof field === "boolean") return String(field);
|
|
85
|
+
if (field === null) return "null";
|
|
86
|
+
try {
|
|
87
|
+
return JSON.stringify(field) ?? "";
|
|
88
|
+
} catch {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function parseStrictNumber(s) {
|
|
93
|
+
if (s.trim() === "") return null;
|
|
94
|
+
const n = Number(s);
|
|
95
|
+
return Number.isFinite(n) ? n : null;
|
|
96
|
+
}
|
|
97
|
+
function compareNumeric(a, b, cmp) {
|
|
98
|
+
const an = parseStrictNumber(a);
|
|
99
|
+
const bn = parseStrictNumber(b);
|
|
100
|
+
if (an === null || bn === null) return false;
|
|
101
|
+
return cmp(an, bn);
|
|
102
|
+
}
|
|
103
|
+
function likeToRegExp(pattern, caseInsensitive) {
|
|
104
|
+
let re = "^";
|
|
105
|
+
for (const ch of pattern) {
|
|
106
|
+
if (ch === "%") re += ".*";
|
|
107
|
+
else if (ch === "_") re += ".";
|
|
108
|
+
else re += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
109
|
+
}
|
|
110
|
+
re += "$";
|
|
111
|
+
try {
|
|
112
|
+
return new RegExp(re, caseInsensitive ? "i" : void 0);
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function matchesRaw(cf, record) {
|
|
118
|
+
if (cf.op === "is") {
|
|
119
|
+
const field2 = record[cf.column];
|
|
120
|
+
switch (cf.value) {
|
|
121
|
+
case "null":
|
|
122
|
+
return field2 === void 0 || field2 === null;
|
|
123
|
+
case "true":
|
|
124
|
+
return field2 === true;
|
|
125
|
+
case "false":
|
|
126
|
+
return field2 === false;
|
|
127
|
+
default:
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (cf.op === "isdistinct") {
|
|
132
|
+
const field2 = record[cf.column];
|
|
133
|
+
const fieldIsNull = field2 === void 0 || field2 === null;
|
|
134
|
+
if (fieldIsNull) return true;
|
|
135
|
+
return jsonValueToString(field2) !== cf.value;
|
|
136
|
+
}
|
|
137
|
+
if (!(cf.column in record) || record[cf.column] === void 0) return null;
|
|
138
|
+
const field = record[cf.column];
|
|
139
|
+
const fieldStr = jsonValueToString(field);
|
|
140
|
+
switch (cf.op) {
|
|
141
|
+
case "eq":
|
|
142
|
+
return fieldStr === cf.value;
|
|
143
|
+
case "neq":
|
|
144
|
+
return fieldStr !== cf.value;
|
|
145
|
+
case "lt":
|
|
146
|
+
return compareNumeric(fieldStr, cf.value, (a, b) => a < b);
|
|
147
|
+
case "lte":
|
|
148
|
+
return compareNumeric(fieldStr, cf.value, (a, b) => a <= b);
|
|
149
|
+
case "gt":
|
|
150
|
+
return compareNumeric(fieldStr, cf.value, (a, b) => a > b);
|
|
151
|
+
case "gte":
|
|
152
|
+
return compareNumeric(fieldStr, cf.value, (a, b) => a >= b);
|
|
153
|
+
case "in":
|
|
154
|
+
return cf.value.split(",").map((v) => v.trim()).some((v) => v === fieldStr);
|
|
155
|
+
case "like": {
|
|
156
|
+
const re = likeToRegExp(cf.value, false);
|
|
157
|
+
return re ? re.test(fieldStr) : null;
|
|
158
|
+
}
|
|
159
|
+
case "ilike": {
|
|
160
|
+
const re = likeToRegExp(cf.value, true);
|
|
161
|
+
return re ? re.test(fieldStr) : null;
|
|
162
|
+
}
|
|
163
|
+
case "match": {
|
|
164
|
+
try {
|
|
165
|
+
return new RegExp(cf.value).test(fieldStr);
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
case "imatch": {
|
|
171
|
+
try {
|
|
172
|
+
return new RegExp(cf.value, "i").test(fieldStr);
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function matchesSingle(cf, record) {
|
|
182
|
+
const raw = matchesRaw(cf, record);
|
|
183
|
+
if (raw === null) return false;
|
|
184
|
+
return cf.negated ? !raw : raw;
|
|
185
|
+
}
|
|
186
|
+
function matchesColumnFilter(filter, record) {
|
|
187
|
+
const set = parseFilterSet(filter);
|
|
188
|
+
if (!set) return false;
|
|
189
|
+
return set.every((cf) => matchesSingle(cf, record));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/AuraChannel.ts
|
|
193
|
+
var AuraChannel = class {
|
|
194
|
+
name;
|
|
195
|
+
_projectId;
|
|
196
|
+
_listeners = [];
|
|
197
|
+
_subscribed = false;
|
|
198
|
+
_sendFn = null;
|
|
199
|
+
_subscribeCallback = null;
|
|
200
|
+
_presenceState = {};
|
|
201
|
+
_isPrivate = false;
|
|
202
|
+
_broadcastAck = false;
|
|
203
|
+
_broadcastSelf = false;
|
|
204
|
+
_presenceKey = "";
|
|
205
|
+
/** ID de subscription retourné par le serveur (utilisé pour le filtre RLS allowed_sub_ids) */
|
|
206
|
+
_subscriptionId = null;
|
|
207
|
+
/** Compteur pour générer des ref_id uniques par connexion */
|
|
208
|
+
_refCounter = 0;
|
|
209
|
+
/** Dernier event ID recu — utilise pour since_id au reconnect */
|
|
210
|
+
_lastEventId = null;
|
|
211
|
+
/** Pending ACK callbacks : ref_id → resolve */
|
|
212
|
+
_pendingAcks = /* @__PURE__ */ new Map();
|
|
213
|
+
constructor(name, projectId, options) {
|
|
214
|
+
this.name = name;
|
|
215
|
+
this._projectId = projectId;
|
|
216
|
+
this._isPrivate = options?.private ?? false;
|
|
217
|
+
this._broadcastAck = options?.broadcast?.ack ?? false;
|
|
218
|
+
this._broadcastSelf = options?.broadcast?.self ?? false;
|
|
219
|
+
this._presenceKey = options?.config?.presence?.key ?? "";
|
|
220
|
+
}
|
|
221
|
+
/** Génère un ref_id unique pour la corrélation request/response. */
|
|
222
|
+
_nextRef() {
|
|
223
|
+
return `${this.fullName}:r${++this._refCounter}`;
|
|
224
|
+
}
|
|
225
|
+
/** Full channel name with project prefix (for server protocol) */
|
|
226
|
+
get fullName() {
|
|
227
|
+
return `${this._projectId}:${this.name}`;
|
|
228
|
+
}
|
|
229
|
+
on(type, filter, callback) {
|
|
230
|
+
this._listeners.push({ type, filter, callback });
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
// ─── Subscribe ──────────────────────────────────────────────────────────
|
|
234
|
+
/**
|
|
235
|
+
* Subscribe to the channel. Sends the Subscribe message to the server
|
|
236
|
+
* with the full config (postgres_changes, broadcast, presence).
|
|
237
|
+
*/
|
|
238
|
+
subscribe(callback) {
|
|
239
|
+
this._subscribeCallback = callback ?? null;
|
|
240
|
+
this._subscribed = true;
|
|
241
|
+
if (this._sendFn) {
|
|
242
|
+
this._doSubscribe();
|
|
243
|
+
}
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
/** Called by RealtimeService once the WS connection is ready. */
|
|
247
|
+
_bindSend(sendFn) {
|
|
248
|
+
this._sendFn = sendFn;
|
|
249
|
+
}
|
|
250
|
+
/** Send the subscribe message to the server. */
|
|
251
|
+
_doSubscribe() {
|
|
252
|
+
if (!this._sendFn) return;
|
|
253
|
+
const pgFilters = this._listeners.filter((l) => l.type === "postgres_changes").map((l) => l.filter);
|
|
254
|
+
const msg = {
|
|
255
|
+
type: "subscribe",
|
|
256
|
+
channel: this.fullName,
|
|
257
|
+
ref_id: this._nextRef(),
|
|
258
|
+
since_id: this._lastEventId ?? void 0,
|
|
259
|
+
config: {
|
|
260
|
+
postgres_changes: pgFilters,
|
|
261
|
+
broadcast: {
|
|
262
|
+
self_broadcast: this._broadcastSelf,
|
|
263
|
+
// ← depuis les options du channel
|
|
264
|
+
ack: this._broadcastAck
|
|
265
|
+
// ← depuis les options du channel
|
|
266
|
+
},
|
|
267
|
+
presence: { key: this._presenceKey },
|
|
268
|
+
// ← depuis les options du channel
|
|
269
|
+
private: this._isPrivate
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
this._sendFn(JSON.stringify(msg));
|
|
273
|
+
}
|
|
274
|
+
/** Unsubscribe from the channel. */
|
|
275
|
+
unsubscribe() {
|
|
276
|
+
if (this._sendFn) {
|
|
277
|
+
this._sendFn(JSON.stringify({ type: "unsubscribe", channel: this.fullName }));
|
|
278
|
+
}
|
|
279
|
+
this._subscribed = false;
|
|
280
|
+
this._listeners.length = 0;
|
|
281
|
+
this._presenceState = {};
|
|
282
|
+
}
|
|
283
|
+
// ─── Broadcast ──────────────────────────────────────────────────────────
|
|
284
|
+
/**
|
|
285
|
+
* Send a broadcast message to the channel.
|
|
286
|
+
* If broadcast.ack=true, returns a Promise that resolves when the server ACKs.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* // Without ACK (fire & forget)
|
|
291
|
+
* channel.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100, y: 200 } })
|
|
292
|
+
*
|
|
293
|
+
* // With ACK (wait for server confirmation)
|
|
294
|
+
* await channel.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100 } })
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
send(message) {
|
|
298
|
+
return new Promise((resolve) => {
|
|
299
|
+
if (!this._sendFn) {
|
|
300
|
+
resolve("error");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const ref_id = message.ref_id ?? this._nextRef();
|
|
304
|
+
if (this._broadcastAck) {
|
|
305
|
+
const timer = setTimeout(() => {
|
|
306
|
+
this._pendingAcks.delete(ref_id);
|
|
307
|
+
resolve("timeout");
|
|
308
|
+
}, 5e3);
|
|
309
|
+
this._pendingAcks.set(ref_id, () => {
|
|
310
|
+
clearTimeout(timer);
|
|
311
|
+
resolve("ok");
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
resolve("ok");
|
|
315
|
+
}
|
|
316
|
+
this._sendFn(
|
|
317
|
+
JSON.stringify({
|
|
318
|
+
type: "broadcast",
|
|
319
|
+
channel: this.fullName,
|
|
320
|
+
ref_id,
|
|
321
|
+
payload: { event: message.event, ...message.payload }
|
|
322
|
+
})
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// ─── Presence ───────────────────────────────────────────────────────────
|
|
327
|
+
/**
|
|
328
|
+
* Track presence state for this client.
|
|
329
|
+
* Returns a Promise that resolves with 'ok' once tracked (compatible Supabase).
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```ts
|
|
333
|
+
* await channel.track({ user: 'user-1', online_at: new Date().toISOString() })
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
async track(state) {
|
|
337
|
+
if (!this._sendFn) return "error";
|
|
338
|
+
this._sendFn(
|
|
339
|
+
JSON.stringify({
|
|
340
|
+
type: "presence_update",
|
|
341
|
+
channel: this.fullName,
|
|
342
|
+
meta: state
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
return "ok";
|
|
346
|
+
}
|
|
347
|
+
/** Stop tracking presence. */
|
|
348
|
+
async untrack() {
|
|
349
|
+
return this.track({});
|
|
350
|
+
}
|
|
351
|
+
/** Get the current presence state for this channel. */
|
|
352
|
+
presenceState() {
|
|
353
|
+
return { ...this._presenceState };
|
|
354
|
+
}
|
|
355
|
+
// ─── Internal: dispatch server messages ──────────────────────────────────
|
|
356
|
+
/** Called by RealtimeService when a server message arrives for this channel. */
|
|
357
|
+
_onMessage(msg) {
|
|
358
|
+
switch (msg.type) {
|
|
359
|
+
case "subscribed": {
|
|
360
|
+
const subMsg = msg;
|
|
361
|
+
if (subMsg.subscription_id) this._subscriptionId = subMsg.subscription_id;
|
|
362
|
+
this._subscribeCallback?.("SUBSCRIBED");
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "event": {
|
|
366
|
+
const evtMsg = msg;
|
|
367
|
+
if (evtMsg.id) {
|
|
368
|
+
const event = {
|
|
369
|
+
id: evtMsg.id,
|
|
370
|
+
event: evtMsg.event,
|
|
371
|
+
topic: evtMsg.topic,
|
|
372
|
+
payload: evtMsg.payload,
|
|
373
|
+
timestamp: evtMsg.timestamp,
|
|
374
|
+
allowed_sub_ids: evtMsg.allowed_sub_ids
|
|
375
|
+
};
|
|
376
|
+
this._dispatchEvent(event);
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case "presence_state": {
|
|
381
|
+
const presMsg = msg;
|
|
382
|
+
if (presMsg.presences) {
|
|
383
|
+
this._presenceState = {};
|
|
384
|
+
for (const p of presMsg.presences) {
|
|
385
|
+
const key = p.user_id || p.client_id;
|
|
386
|
+
if (!this._presenceState[key]) this._presenceState[key] = [];
|
|
387
|
+
this._presenceState[key].push(p);
|
|
388
|
+
}
|
|
389
|
+
for (const l of this._listeners) {
|
|
390
|
+
if (l.type === "presence" && l.filter.event === "sync") {
|
|
391
|
+
l.callback(this._presenceState);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case "error": {
|
|
398
|
+
const errMsg = msg;
|
|
399
|
+
this._subscribeCallback?.("CHANNEL_ERROR", new Error(errMsg.message ?? "Unknown error"));
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case "token_refreshed":
|
|
403
|
+
break;
|
|
404
|
+
case "broadcast_ack": {
|
|
405
|
+
const ackMsg = msg;
|
|
406
|
+
const refId = ackMsg.ref_id ?? ackMsg.message_id;
|
|
407
|
+
if (refId) {
|
|
408
|
+
const resolve = this._pendingAcks.get(refId);
|
|
409
|
+
if (resolve) {
|
|
410
|
+
this._pendingAcks.delete(refId);
|
|
411
|
+
resolve();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/** Dispatch a RealtimeEvent to matching listeners. */
|
|
419
|
+
_dispatchEvent(event) {
|
|
420
|
+
if (!event) return;
|
|
421
|
+
if (event.id) this._lastEventId = event.id;
|
|
422
|
+
const allowed = event.allowed_sub_ids;
|
|
423
|
+
if (allowed && this._subscriptionId && !allowed.includes(this._subscriptionId)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const eventType = event.event;
|
|
427
|
+
if (eventType === "presence") {
|
|
428
|
+
const payload = event.payload;
|
|
429
|
+
const action = payload.action;
|
|
430
|
+
const key = payload.user_id || payload.client_id;
|
|
431
|
+
if (action === "join") {
|
|
432
|
+
if (!this._presenceState[key]) this._presenceState[key] = [];
|
|
433
|
+
if (!this._presenceState[key].some((p) => p.client_id === payload.client_id)) {
|
|
434
|
+
const presenceInfo = {
|
|
435
|
+
client_id: payload.client_id,
|
|
436
|
+
user_id: payload.user_id ?? void 0,
|
|
437
|
+
metadata: payload.metadata ?? payload.meta ?? {},
|
|
438
|
+
joined_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
439
|
+
last_seen: (/* @__PURE__ */ new Date()).toISOString()
|
|
440
|
+
};
|
|
441
|
+
this._presenceState[key].push(presenceInfo);
|
|
442
|
+
}
|
|
443
|
+
} else if (action === "leave") {
|
|
444
|
+
const arr = this._presenceState[key];
|
|
445
|
+
if (arr) {
|
|
446
|
+
const idx = arr.findIndex((p) => p.client_id === payload.client_id);
|
|
447
|
+
if (idx >= 0) arr.splice(idx, 1);
|
|
448
|
+
if (arr.length === 0) delete this._presenceState[key];
|
|
449
|
+
}
|
|
450
|
+
} else if (action === "update") {
|
|
451
|
+
const arr = this._presenceState[key];
|
|
452
|
+
if (arr) {
|
|
453
|
+
const idx = arr.findIndex((p) => p.client_id === payload.client_id);
|
|
454
|
+
if (idx >= 0) {
|
|
455
|
+
arr[idx].metadata = payload.metadata ?? payload.meta ?? {};
|
|
456
|
+
arr[idx].last_seen = (/* @__PURE__ */ new Date()).toISOString();
|
|
457
|
+
} else {
|
|
458
|
+
const presenceInfo = {
|
|
459
|
+
client_id: payload.client_id,
|
|
460
|
+
user_id: payload.user_id ?? void 0,
|
|
461
|
+
metadata: payload.metadata ?? payload.meta ?? {},
|
|
462
|
+
joined_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
463
|
+
last_seen: (/* @__PURE__ */ new Date()).toISOString()
|
|
464
|
+
};
|
|
465
|
+
this._presenceState[key].push(presenceInfo);
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
this._presenceState[key] = [{
|
|
469
|
+
client_id: payload.client_id,
|
|
470
|
+
user_id: payload.user_id ?? void 0,
|
|
471
|
+
metadata: payload.metadata ?? payload.meta ?? {},
|
|
472
|
+
joined_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
473
|
+
last_seen: (/* @__PURE__ */ new Date()).toISOString()
|
|
474
|
+
}];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (const listener of this._listeners) {
|
|
479
|
+
if (listener.type === "postgres_changes") {
|
|
480
|
+
const filter = listener.filter;
|
|
481
|
+
if (eventType === "insert" || eventType === "update" || eventType === "delete") {
|
|
482
|
+
const filterEvent = filter.event.toLowerCase();
|
|
483
|
+
if (filterEvent === "*" || filterEvent === eventType) {
|
|
484
|
+
const payload = event.payload;
|
|
485
|
+
if (filter.schema && payload.schema !== filter.schema) continue;
|
|
486
|
+
if (filter.table && payload.table !== filter.table) continue;
|
|
487
|
+
if (filter.filter) {
|
|
488
|
+
const target = eventType === "delete" ? payload.old_record : payload.record;
|
|
489
|
+
if (target && typeof target === "object") {
|
|
490
|
+
if (!matchesColumnFilter(filter.filter, target)) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
listener.callback(event);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} else if (listener.type === "broadcast") {
|
|
499
|
+
if (eventType === "broadcast") {
|
|
500
|
+
const filter = listener.filter;
|
|
501
|
+
const payload = event.payload;
|
|
502
|
+
if (!filter.event || payload.event === filter.event) {
|
|
503
|
+
listener.callback(event);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} else if (listener.type === "presence") {
|
|
507
|
+
if (eventType === "presence") {
|
|
508
|
+
const filter = listener.filter;
|
|
509
|
+
const payload = event.payload;
|
|
510
|
+
const action = payload.action;
|
|
511
|
+
if (filter.event === "join" && action === "join") {
|
|
512
|
+
listener.callback({
|
|
513
|
+
key: payload.user_id || payload.client_id,
|
|
514
|
+
newPresences: [payload]
|
|
515
|
+
});
|
|
516
|
+
} else if (filter.event === "leave" && action === "leave") {
|
|
517
|
+
listener.callback({
|
|
518
|
+
key: payload.user_id || payload.client_id,
|
|
519
|
+
leftPresences: [payload]
|
|
520
|
+
});
|
|
521
|
+
} else if (filter.event === "sync") {
|
|
522
|
+
listener.callback(this.presenceState());
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/** Whether this channel has been subscribed. */
|
|
529
|
+
get isSubscribed() {
|
|
530
|
+
return this._subscribed;
|
|
531
|
+
}
|
|
532
|
+
/** Get listeners (for re-subscribe after reconnect). */
|
|
533
|
+
_getListeners() {
|
|
534
|
+
return this._listeners;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/HeartbeatManager.ts
|
|
539
|
+
var HeartbeatManager = class {
|
|
540
|
+
_worker = null;
|
|
541
|
+
_workerUrl = null;
|
|
542
|
+
_interval = null;
|
|
543
|
+
_sendFn = null;
|
|
544
|
+
start(sendPing, intervalMs = 25e3) {
|
|
545
|
+
this.stop();
|
|
546
|
+
this._sendFn = sendPing;
|
|
547
|
+
if (typeof Worker !== "undefined" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
|
|
548
|
+
try {
|
|
549
|
+
const code = `let i=null;onmessage=(e)=>{if(e.data.type==='start'){if(i)clearInterval(i);i=setInterval(()=>postMessage('ping'),e.data.interval||25000);}else if(e.data.type==='stop'){if(i){clearInterval(i);i=null;}}};`;
|
|
550
|
+
const blob = new Blob([code], { type: "application/javascript" });
|
|
551
|
+
this._workerUrl = URL.createObjectURL(blob);
|
|
552
|
+
this._worker = new Worker(this._workerUrl);
|
|
553
|
+
this._worker.onmessage = () => {
|
|
554
|
+
this._sendFn?.();
|
|
555
|
+
};
|
|
556
|
+
this._worker.postMessage({ type: "start", interval: intervalMs });
|
|
557
|
+
return;
|
|
558
|
+
} catch {
|
|
559
|
+
this._worker = null;
|
|
560
|
+
if (this._workerUrl) {
|
|
561
|
+
try {
|
|
562
|
+
URL.revokeObjectURL(this._workerUrl);
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
this._workerUrl = null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
this._interval = setInterval(() => {
|
|
570
|
+
this._sendFn?.();
|
|
571
|
+
}, intervalMs);
|
|
572
|
+
}
|
|
573
|
+
stop() {
|
|
574
|
+
if (this._worker) {
|
|
575
|
+
try {
|
|
576
|
+
this._worker.postMessage({ type: "stop" });
|
|
577
|
+
this._worker.terminate();
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
this._worker = null;
|
|
581
|
+
}
|
|
582
|
+
if (this._workerUrl) {
|
|
583
|
+
try {
|
|
584
|
+
URL.revokeObjectURL(this._workerUrl);
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
this._workerUrl = null;
|
|
588
|
+
}
|
|
589
|
+
if (this._interval) {
|
|
590
|
+
clearInterval(this._interval);
|
|
591
|
+
this._interval = null;
|
|
592
|
+
}
|
|
593
|
+
this._sendFn = null;
|
|
594
|
+
}
|
|
595
|
+
get isActive() {
|
|
596
|
+
return this._worker !== null || this._interval !== null;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// src/RealtimeService.ts
|
|
601
|
+
var RealtimeService = class _RealtimeService {
|
|
602
|
+
client;
|
|
603
|
+
_ws = null;
|
|
604
|
+
_channels = /* @__PURE__ */ new Map();
|
|
605
|
+
_state = "disconnected";
|
|
606
|
+
_stateListeners = /* @__PURE__ */ new Set();
|
|
607
|
+
// ─── Reconnection state ──────────────────────────────────────────────
|
|
608
|
+
_autoReconnect = true;
|
|
609
|
+
_reconnectAttempts = 0;
|
|
610
|
+
_maxReconnectAttempts = Infinity;
|
|
611
|
+
_reconnectTimer = null;
|
|
612
|
+
_intentionalClose = false;
|
|
613
|
+
_heartbeat = new HeartbeatManager();
|
|
614
|
+
_tokenRefreshTimer = null;
|
|
615
|
+
/** Dernier access_token propagé au serveur — évite les pushs redondants (propagation différentielle). */
|
|
616
|
+
_lastSentToken = null;
|
|
617
|
+
/** Backoff delays in ms: 1s, 2s, 4s, 8s, 16s, 30s (cap) */
|
|
618
|
+
static BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
619
|
+
constructor(client) {
|
|
620
|
+
this.client = client;
|
|
621
|
+
}
|
|
622
|
+
/** Current connection state. */
|
|
623
|
+
get connectionState() {
|
|
624
|
+
return this._state;
|
|
625
|
+
}
|
|
626
|
+
/** Listen for connection state changes. */
|
|
627
|
+
onStateChange(callback) {
|
|
628
|
+
this._stateListeners.add(callback);
|
|
629
|
+
return () => this._stateListeners.delete(callback);
|
|
630
|
+
}
|
|
631
|
+
_setState(state) {
|
|
632
|
+
this._state = state;
|
|
633
|
+
for (const cb of this._stateListeners) {
|
|
634
|
+
try {
|
|
635
|
+
cb(state);
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// ─── WebSocket URL ──────────────────────────────────────────────────────
|
|
641
|
+
get _wsUrl() {
|
|
642
|
+
const base = this.client.baseUrl.replace(/^http/, "ws");
|
|
643
|
+
return `${base}/v1/realtime/ws`;
|
|
644
|
+
}
|
|
645
|
+
// ─── Connection management ──────────────────────────────────────────────
|
|
646
|
+
/** Ensure the WebSocket connection is open. */
|
|
647
|
+
connect() {
|
|
648
|
+
if (this._ws && (this._ws.readyState === WebSocket.OPEN || this._ws.readyState === WebSocket.CONNECTING)) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
this._intentionalClose = false;
|
|
652
|
+
this._setState("connecting");
|
|
653
|
+
const token = this.client.authStore.token;
|
|
654
|
+
const params = new URLSearchParams();
|
|
655
|
+
if (token) params.set("token", token);
|
|
656
|
+
if (this.client.apiKey) params.set("apikey", this.client.apiKey);
|
|
657
|
+
const url = `${this._wsUrl}?${params.toString()}`;
|
|
658
|
+
const ws = new WebSocket(url);
|
|
659
|
+
this._ws = ws;
|
|
660
|
+
ws.onopen = () => {
|
|
661
|
+
if (ws !== this._ws) {
|
|
662
|
+
try {
|
|
663
|
+
ws.close();
|
|
664
|
+
} catch {
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
this._reconnectAttempts = 0;
|
|
669
|
+
this._setState("connected");
|
|
670
|
+
this._heartbeat.start(() => this._send(JSON.stringify({ type: "ping" })), 25e3);
|
|
671
|
+
this._scheduleTokenRefresh();
|
|
672
|
+
for (const channel of this._channels.values()) {
|
|
673
|
+
channel._bindSend((data) => this._send(data));
|
|
674
|
+
if (channel.isSubscribed) {
|
|
675
|
+
channel._doSubscribe();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
ws.onmessage = (evt) => {
|
|
680
|
+
if (ws !== this._ws) return;
|
|
681
|
+
try {
|
|
682
|
+
const raw = typeof evt.data === "string" ? evt.data : "";
|
|
683
|
+
const msg = JSON.parse(raw);
|
|
684
|
+
this._dispatch(msg);
|
|
685
|
+
} catch (e) {
|
|
686
|
+
if (typeof console !== "undefined") {
|
|
687
|
+
console.warn("[aura-realtime] Failed to parse WS message:", evt.data, e);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
ws.onclose = () => {
|
|
692
|
+
if (ws !== this._ws) return;
|
|
693
|
+
this._ws = null;
|
|
694
|
+
this._lastSentToken = null;
|
|
695
|
+
this._heartbeat.stop();
|
|
696
|
+
if (this._intentionalClose) {
|
|
697
|
+
this._setState("disconnected");
|
|
698
|
+
this._intentionalClose = false;
|
|
699
|
+
} else if (this._autoReconnect) {
|
|
700
|
+
this._scheduleReconnect();
|
|
701
|
+
} else {
|
|
702
|
+
this._setState("disconnected");
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
ws.onerror = (evt) => {
|
|
706
|
+
if (ws !== this._ws) return;
|
|
707
|
+
if (typeof console !== "undefined") {
|
|
708
|
+
console.warn("[aura-realtime] WebSocket error", evt);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/** Send raw data through the WebSocket. */
|
|
713
|
+
_send(data) {
|
|
714
|
+
if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
|
715
|
+
this._ws.send(data);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/** Refresh the JWT token on an existing connection (no disconnect). */
|
|
719
|
+
refreshToken(token) {
|
|
720
|
+
if (token === this._lastSentToken) {
|
|
721
|
+
this._scheduleTokenRefresh();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
this._lastSentToken = token;
|
|
725
|
+
this._send(JSON.stringify({ type: "access_token", token }));
|
|
726
|
+
this._scheduleTokenRefresh();
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Définit/rafraîchit explicitement le token utilisé pour l'autorisation des
|
|
730
|
+
* canaux privés et broadcast (parité `@supabase/realtime-js` `setAuth`).
|
|
731
|
+
*
|
|
732
|
+
* - Avec `token` : utilise ce token. Sinon, reprend `client.authStore.token`.
|
|
733
|
+
* - Propage via le pipeline existant `refreshToken` (push
|
|
734
|
+
* `{ type:'access_token', token }` sur une connexion ouverte, dédupliqué via
|
|
735
|
+
* `_lastSentToken`). N'ouvre ni ne ferme aucune connexion WS.
|
|
736
|
+
* - Hors-ligne : `connect()` lit `client.authStore.token` au moment de l'open,
|
|
737
|
+
* donc `setAuth()` doit être appelé avec un token présent dans le store (ou
|
|
738
|
+
* après authentification) pour prendre effet sur une future connexion.
|
|
739
|
+
*
|
|
740
|
+
* À appeler AVANT de s'abonner à un canal privé (`channel(name,{private:true})`).
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```ts
|
|
744
|
+
* aura.realtime.setAuth(session.access_token)
|
|
745
|
+
* const ch = aura.realtime.channel('secret', { private: true }).subscribe()
|
|
746
|
+
* ```
|
|
747
|
+
*/
|
|
748
|
+
setAuth(token) {
|
|
749
|
+
const resolved = token ?? this.client.authStore.token;
|
|
750
|
+
if (resolved == null) return;
|
|
751
|
+
this.refreshToken(resolved);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Envoie un broadcast par HTTP (REST), sans connexion WebSocket ouverte
|
|
755
|
+
* (parité `@supabase/realtime-js`). Utile côté serveur/edge.
|
|
756
|
+
*
|
|
757
|
+
* POST `/v1/realtime/{projectId}/broadcast` avec le contrat du handler backend
|
|
758
|
+
* (`broadcast.rs`) : `{ channel, payload }`, `channel` préfixé par le projet,
|
|
759
|
+
* `event` replié dans `payload` (aligné sur le broadcast WS).
|
|
760
|
+
*
|
|
761
|
+
* @param channel Nom du canal (préfixé projet automatiquement si absent).
|
|
762
|
+
* @param event Nom de l'événement broadcast.
|
|
763
|
+
* @param payload Données de l'événement.
|
|
764
|
+
* @returns `AuraResponse<null>` — `error` non nul en cas de 4xx/rate-limit.
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* ```ts
|
|
768
|
+
* await aura.realtime.httpBroadcast('room-1', 'cursor', { x: 10, y: 20 })
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
httpBroadcast(channel, event, payload = {}) {
|
|
772
|
+
const prefix = `${this.client.projectId}:`;
|
|
773
|
+
const fullChannel = channel.startsWith(prefix) ? channel : `${prefix}${channel}`;
|
|
774
|
+
const body = {
|
|
775
|
+
channel: fullChannel,
|
|
776
|
+
payload: { event, ...payload }
|
|
777
|
+
};
|
|
778
|
+
return fetchWithAuth(
|
|
779
|
+
`${this.client.baseUrl}/v1/realtime/${this.client.projectId}/broadcast`,
|
|
780
|
+
{ method: "POST", body },
|
|
781
|
+
this.client
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Schedule proactive token refresh at 80% of the JWT TTL.
|
|
786
|
+
* Prevents token expiration during an active WebSocket session.
|
|
787
|
+
*/
|
|
788
|
+
_scheduleTokenRefresh() {
|
|
789
|
+
if (this._tokenRefreshTimer) {
|
|
790
|
+
clearTimeout(this._tokenRefreshTimer);
|
|
791
|
+
this._tokenRefreshTimer = null;
|
|
792
|
+
}
|
|
793
|
+
const token = this.client.authStore.token;
|
|
794
|
+
if (!token) return;
|
|
795
|
+
try {
|
|
796
|
+
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
797
|
+
const exp = payload.exp * 1e3;
|
|
798
|
+
const ttl = exp - Date.now();
|
|
799
|
+
if (ttl <= 0) return;
|
|
800
|
+
const refreshIn = ttl * 0.8;
|
|
801
|
+
this._tokenRefreshTimer = setTimeout(async () => {
|
|
802
|
+
this._tokenRefreshTimer = null;
|
|
803
|
+
try {
|
|
804
|
+
const res = await this.client.auth.refreshSession();
|
|
805
|
+
if (res.data) {
|
|
806
|
+
this.refreshToken(res.data.access_token);
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
}
|
|
810
|
+
}, refreshIn);
|
|
811
|
+
} catch {
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/** Schedule a reconnection attempt with exponential backoff + jitter. */
|
|
815
|
+
_scheduleReconnect() {
|
|
816
|
+
if (this._reconnectTimer) {
|
|
817
|
+
clearTimeout(this._reconnectTimer);
|
|
818
|
+
this._reconnectTimer = null;
|
|
819
|
+
}
|
|
820
|
+
if (this._reconnectAttempts >= this._maxReconnectAttempts) {
|
|
821
|
+
this._setState("disconnected");
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
this._reconnectAttempts++;
|
|
825
|
+
this._setState("reconnecting");
|
|
826
|
+
const idx = Math.min(this._reconnectAttempts - 1, _RealtimeService.BACKOFF_MS.length - 1);
|
|
827
|
+
const baseDelay = _RealtimeService.BACKOFF_MS[idx];
|
|
828
|
+
const jitter = Math.random() * baseDelay * 0.25;
|
|
829
|
+
const delay = baseDelay + jitter;
|
|
830
|
+
this._reconnectTimer = setTimeout(() => {
|
|
831
|
+
this._reconnectTimer = null;
|
|
832
|
+
this.connect();
|
|
833
|
+
}, delay);
|
|
834
|
+
}
|
|
835
|
+
/** Enable/disable auto-reconnect (enabled by default). */
|
|
836
|
+
setAutoReconnect(enabled, maxAttempts) {
|
|
837
|
+
this._autoReconnect = enabled;
|
|
838
|
+
if (maxAttempts !== void 0) this._maxReconnectAttempts = maxAttempts;
|
|
839
|
+
}
|
|
840
|
+
/** Disconnect the WebSocket and clean up all channels. */
|
|
841
|
+
disconnect() {
|
|
842
|
+
this._intentionalClose = true;
|
|
843
|
+
this._heartbeat.stop();
|
|
844
|
+
if (this._tokenRefreshTimer) {
|
|
845
|
+
clearTimeout(this._tokenRefreshTimer);
|
|
846
|
+
this._tokenRefreshTimer = null;
|
|
847
|
+
}
|
|
848
|
+
if (this._reconnectTimer) {
|
|
849
|
+
clearTimeout(this._reconnectTimer);
|
|
850
|
+
this._reconnectTimer = null;
|
|
851
|
+
}
|
|
852
|
+
this._reconnectAttempts = 0;
|
|
853
|
+
for (const channel of this._channels.values()) {
|
|
854
|
+
channel.unsubscribe();
|
|
855
|
+
}
|
|
856
|
+
this._channels.clear();
|
|
857
|
+
if (this._ws) {
|
|
858
|
+
this._ws.close();
|
|
859
|
+
this._ws = null;
|
|
860
|
+
}
|
|
861
|
+
this._setState("disconnected");
|
|
862
|
+
this._lastSentToken = null;
|
|
863
|
+
}
|
|
864
|
+
// ─── Channel factory ────────────────────────────────────────────────────
|
|
865
|
+
/**
|
|
866
|
+
* Create or retrieve a channel by name.
|
|
867
|
+
* Returns an `AuraChannel` with chainable `.on()` / `.subscribe()` API.
|
|
868
|
+
*
|
|
869
|
+
* @example
|
|
870
|
+
* ```ts
|
|
871
|
+
* const channel = aura.realtime.channel('room-1')
|
|
872
|
+
* ```
|
|
873
|
+
*/
|
|
874
|
+
channel(name, options) {
|
|
875
|
+
const existing = this._channels.get(name);
|
|
876
|
+
if (existing) return existing;
|
|
877
|
+
const ch = new AuraChannel(name, this.client.projectId, options);
|
|
878
|
+
this._channels.set(name, ch);
|
|
879
|
+
this.connect();
|
|
880
|
+
if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
|
881
|
+
ch._bindSend((data) => this._send(data));
|
|
882
|
+
}
|
|
883
|
+
return ch;
|
|
884
|
+
}
|
|
885
|
+
/** Remove a channel. */
|
|
886
|
+
removeChannel(name) {
|
|
887
|
+
const ch = this._channels.get(name);
|
|
888
|
+
if (ch) {
|
|
889
|
+
ch.unsubscribe();
|
|
890
|
+
this._channels.delete(name);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/** List active channels. */
|
|
894
|
+
getChannels() {
|
|
895
|
+
return Array.from(this._channels.values());
|
|
896
|
+
}
|
|
897
|
+
/** Remove all channels and subscriptions. */
|
|
898
|
+
removeAllChannels() {
|
|
899
|
+
for (const ch of this._channels.values()) {
|
|
900
|
+
ch.unsubscribe();
|
|
901
|
+
}
|
|
902
|
+
this._channels.clear();
|
|
903
|
+
}
|
|
904
|
+
// ─── Message dispatch ───────────────────────────────────────────────────
|
|
905
|
+
/** Route incoming server messages to the correct channel. */
|
|
906
|
+
_dispatch(msg) {
|
|
907
|
+
const channelFull = "channel" in msg ? msg.channel : "topic" in msg ? msg.topic : void 0;
|
|
908
|
+
if (channelFull) {
|
|
909
|
+
const prefix = `${this.client.projectId}:`;
|
|
910
|
+
const name = channelFull.startsWith(prefix) ? channelFull.slice(prefix.length) : channelFull;
|
|
911
|
+
const ch = this._channels.get(name);
|
|
912
|
+
if (ch) {
|
|
913
|
+
ch._onMessage(msg);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
for (const [, channel] of this._channels) {
|
|
917
|
+
if (channel.fullName === channelFull || channelFull.startsWith(channel.fullName + ":")) {
|
|
918
|
+
channel._onMessage(msg);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (msg.type === "token_refreshed") {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (msg.type === "pong") {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (msg.type === "broadcast_ack") {
|
|
930
|
+
const ack = msg;
|
|
931
|
+
const refId = ack.ref_id ?? ack.message_id;
|
|
932
|
+
if (refId) {
|
|
933
|
+
const marker = refId.lastIndexOf(":r");
|
|
934
|
+
if (marker > 0) {
|
|
935
|
+
const fullName = refId.slice(0, marker);
|
|
936
|
+
for (const ch of this._channels.values()) {
|
|
937
|
+
if (ch.fullName === fullName) {
|
|
938
|
+
ch._onMessage(msg);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
for (const ch of this._channels.values()) {
|
|
945
|
+
ch._onMessage(msg);
|
|
946
|
+
}
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (msg.type === "error") {
|
|
950
|
+
for (const ch of this._channels.values()) {
|
|
951
|
+
ch._onMessage(msg);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// ─── Backward compatibility ─────────────────────────────────────────────
|
|
956
|
+
/**
|
|
957
|
+
* Simple callback-based subscription (legacy API).
|
|
958
|
+
* Prefer using `.channel().on().subscribe()` instead.
|
|
959
|
+
*
|
|
960
|
+
* @deprecated Use `channel().on().subscribe()` instead.
|
|
961
|
+
*/
|
|
962
|
+
on(channelName, callback) {
|
|
963
|
+
this.connect();
|
|
964
|
+
const ch = this.channel(channelName);
|
|
965
|
+
ch.on("postgres_changes", { event: "*" }, callback);
|
|
966
|
+
ch.subscribe();
|
|
967
|
+
return {
|
|
968
|
+
unsubscribe: () => this.removeChannel(channelName)
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
export { AuraChannel, HeartbeatManager, RealtimeService };
|