@protobase/server 0.51.2 → 0.52.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/dist/aggregate-groups.js +2 -2
- package/dist/aggregate-service.js +4 -4
- package/dist/aggregate-splits.js +1 -1
- package/dist/assistant/batch-approval.js +49 -51
- package/dist/assistant/batch-before.js +38 -0
- package/dist/assistant/batch-echo.js +36 -0
- package/dist/assistant/batch-write-args.js +14 -5
- package/dist/assistant/batch-write-tool.js +30 -11
- package/dist/assistant/budgeted-page.js +30 -0
- package/dist/assistant/comment-tools.d.ts +20 -0
- package/dist/assistant/comment-tools.js +120 -0
- package/dist/assistant/count-records-tool.d.ts +5 -0
- package/dist/assistant/count-records-tool.js +84 -0
- package/dist/assistant/query-result-budget.js +32 -0
- package/dist/assistant/query-tools.js +7 -7
- package/dist/assistant/record-before.js +2 -1
- package/dist/assistant/record-field-piece.js +54 -0
- package/dist/assistant/record-tool-args.js +6 -3
- package/dist/assistant/record-tools.d.ts +4 -1
- package/dist/assistant/record-tools.js +17 -6
- package/dist/assistant/records.d.ts +19 -0
- package/dist/assistant/records.js +20 -13
- package/dist/assistant/result-budget.js +72 -0
- package/dist/assistant/tools.d.ts +3 -0
- package/dist/assistant/write-echo.js +41 -0
- package/dist/assistant/write-messages.js +26 -6
- package/dist/assistant/write-prompts.js +37 -12
- package/dist/assistant/write-record-tools.js +15 -6
- package/dist/batch/run-batch.js +1 -1
- package/dist/better-auth/auth-settings.d.ts +1 -1
- package/dist/changes/capture-schema.js +1 -1
- package/dist/changes/resource-change.d.ts +4 -1
- package/dist/changes/resource-change.js +27 -3
- package/dist/comments/post-comment.js +1 -1
- package/dist/comments/read-comments.js +1 -1
- package/dist/comments/tool-comment-types.d.ts +28 -0
- package/dist/comments/tool-comments.js +30 -0
- package/dist/create-admin.d.ts +5 -2
- package/dist/create-admin.js +20 -10
- package/dist/deps.d.ts +1 -1
- package/dist/encryption/scan.js +1 -1
- package/dist/error-handler.js +3 -2
- package/dist/etag.js +1 -1
- package/dist/files/download.js +1 -1
- package/dist/files/file-cleanup.d.ts +1 -1
- package/dist/functions/records.d.ts +1 -1
- package/dist/functions/records.js +2 -2
- package/dist/history/read-history.js +2 -2
- package/dist/index.d.ts +9 -3
- package/dist/index.js +9 -4
- package/dist/jobs/run-job.d.ts +1 -1
- package/dist/list-photos.js +2 -2
- package/dist/list-service.js +6 -6
- package/dist/map-service.js +2 -2
- package/dist/mcp/mcp-context.d.ts +7 -4
- package/dist/mcp/mcp-context.js +6 -4
- package/dist/mcp/mcp-server.d.ts +3 -2
- package/dist/mcp/mcp-tools.d.ts +6 -1
- package/dist/mcp/mcp-tools.js +8 -4
- package/dist/package.js +1 -1
- package/dist/read-service.js +2 -2
- package/dist/realtime/broadcast-api.js +3 -3
- package/dist/realtime/broadcast-frame.js +16 -8
- package/dist/realtime/broadcast-hub.js +16 -6
- package/dist/realtime/broadcast-request.js +1 -1
- package/dist/realtime/change-checker.js +1 -1
- package/dist/realtime/channel-access.js +1 -1
- package/dist/realtime/database-broadcasts.js +83 -0
- package/dist/realtime/presence-hub.js +74 -39
- package/dist/realtime/realtime-app.js +39 -3
- package/dist/realtime/realtime-routes.js +12 -9
- package/dist/realtime/realtime-socket.js +10 -7
- package/dist/realtime/relay/frame-cursor.js +38 -0
- package/dist/realtime/relay/presence-replica.js +57 -0
- package/dist/realtime/relay/realtime-options.d.ts +13 -0
- package/dist/realtime/relay/realtime-options.js +9 -0
- package/dist/realtime/relay/realtime-relay.js +104 -0
- package/dist/realtime/relay/relay-frame-sql.js +82 -0
- package/dist/realtime/relay/relay-presence-sql.js +71 -0
- package/dist/realtime/relay/relay-schema-sql.js +55 -0
- package/dist/realtime/relay/relay-schema.d.ts +7 -0
- package/dist/realtime/relay/relay-schema.js +18 -0
- package/dist/realtime/relay/relayed-hubs.js +61 -0
- package/dist/records.js +1 -1
- package/dist/related-labels.js +1 -1
- package/dist/rest/rest-context.js +1 -1
- package/dist/routes/batch.js +1 -1
- package/dist/routes/collection.js +2 -2
- package/dist/routes/record.js +3 -3
- package/dist/row-permissions.js +1 -1
- package/dist/sql-parts.js +1 -1
- package/dist/web-app/mcp-icons.d.ts +9 -0
- package/dist/web-app/mcp-icons.js +26 -0
- package/dist/webhooks/subscriber-view.js +27 -2
- package/dist/webhooks/webhooks.js +41 -12
- package/dist/write-pipeline.js +1 -1
- package/dist/write-shared.js +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -25,6 +25,8 @@ import { apiKeySettings, apiKeyVariables } from "./api-keys/api-key-settings.js"
|
|
|
25
25
|
import { apiKeyCaller } from "./api-keys/resolve-caller.js";
|
|
26
26
|
import { jwtAuthenticator, jwtVerifier } from "./auth/jwt.js";
|
|
27
27
|
import { realtimeLimits } from "./realtime/realtime-limits.js";
|
|
28
|
+
import { relayed } from "./realtime/relay/realtime-options.js";
|
|
29
|
+
import { backgroundSettled, waitUntil } from "./functions/wait-until.js";
|
|
28
30
|
import { objectKey } from "./storage/object-key.js";
|
|
29
31
|
import { appNameOver, defaultAppName, shownAppName } from "./app-name.js";
|
|
30
32
|
import { supabasePath } from "./supabase-paths.js";
|
|
@@ -36,20 +38,22 @@ import { readOperatorSettings } from "./better-auth/operator-provider.js";
|
|
|
36
38
|
import { readPlatformSignIn } from "./better-auth/platform-sign-in.js";
|
|
37
39
|
import { normalizeRoles } from "./better-auth/create-auth.js";
|
|
38
40
|
import { mcpApprovals } from "./mcp/mcp-approval-store.js";
|
|
41
|
+
import { changesMigration, checkChanges, migrationCommand } from "./changes/capture-schema.js";
|
|
42
|
+
import { checkComments, commentsMigration } from "./comments/comments-sql.js";
|
|
39
43
|
import { mcpToolContext } from "./mcp/mcp-context.js";
|
|
40
44
|
import { mcpGate, sessionOf } from "./mcp/mcp-gate.js";
|
|
41
45
|
import { mcpHandler } from "./mcp/mcp-handler.js";
|
|
42
46
|
import { mcpRoutes } from "./mcp/mcp-routes.js";
|
|
43
47
|
import { mcpServer } from "./mcp/mcp-server.js";
|
|
48
|
+
import { countRecordsTool } from "./assistant/count-records-tool.js";
|
|
44
49
|
import { getRecordTool, listRecordsTool, listResourcesTool, readRecordTools } from "./assistant/record-tools.js";
|
|
45
50
|
import { deleteRecordTool } from "./assistant/delete-record-tool.js";
|
|
46
51
|
import { createRecordTool, updateRecordTool, writeRecordTools } from "./assistant/write-record-tools.js";
|
|
47
52
|
import { batchWriteTool } from "./assistant/batch-write-tool.js";
|
|
53
|
+
import { addCommentTool, commentTools, listCommentsTool } from "./assistant/comment-tools.js";
|
|
48
54
|
import { mcpTools } from "./mcp/mcp-tools.js";
|
|
49
55
|
import { addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
|
|
50
56
|
import { createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
|
|
51
|
-
import { changesMigration, checkChanges, migrationCommand } from "./changes/capture-schema.js";
|
|
52
|
-
import { checkComments, commentsMigration } from "./comments/comments-sql.js";
|
|
53
57
|
import { coalesce } from "./changes/coalesce.js";
|
|
54
58
|
import { eventMatcher } from "./changes/match-events.js";
|
|
55
59
|
import { parseColumns, readOutbox } from "./changes/outbox.js";
|
|
@@ -87,6 +91,7 @@ import { checkSourceNames, sourceOptions, sourcesFor, sourcesVariable } from "./
|
|
|
87
91
|
import { renderVariant, thumbHash } from "./files/images.js";
|
|
88
92
|
import { pngIcons } from "./web-app/app-icons.js";
|
|
89
93
|
import { webManifestPath } from "./web-app/web-manifest-route.js";
|
|
94
|
+
import { mcpIcons } from "./web-app/mcp-icons.js";
|
|
90
95
|
import { createAdmin } from "./create-admin.js";
|
|
91
96
|
import { handleDroppedConnections } from "./dropped-connections.js";
|
|
92
97
|
import { poolWaitMs, readDbConnections } from "./app-pool.js";
|
|
@@ -101,6 +106,7 @@ import { authStoreTables } from "./better-auth/auth-tables.js";
|
|
|
101
106
|
import { checkResourcesOutsideAuthStore } from "./better-auth/store/resources-outside.js";
|
|
102
107
|
import { checkComputed, computedMigration } from "./computed/computed-schema.js";
|
|
103
108
|
import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
|
|
109
|
+
import { checkRealtimeRelay, realtimeRelayMigration } from "./realtime/relay/relay-schema.js";
|
|
104
110
|
import { checkAi } from "./ai/check-ai.js";
|
|
105
111
|
import { checkNotificationsSchema, notificationsMigration } from "./notifications/schema.js";
|
|
106
112
|
import { sendNotification } from "./notifications/send-notification.js";
|
|
@@ -111,7 +117,6 @@ import { functionApp } from "./functions/function-app.js";
|
|
|
111
117
|
import { readJson } from "./functions/read-json.js";
|
|
112
118
|
import { verifySignature, verifyStandardWebhook } from "./functions/verify-signature.js";
|
|
113
119
|
import { requireEnv } from "./functions/require-env.js";
|
|
114
|
-
import { backgroundSettled, waitUntil } from "./functions/wait-until.js";
|
|
115
120
|
import { storeFile } from "./files/store-file.js";
|
|
116
121
|
import { createFileCleanup } from "./files/file-cleanup.js";
|
|
117
122
|
import { createEncryptionTools } from "./encryption/tools.js";
|
|
@@ -123,4 +128,4 @@ import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
|
123
128
|
import { composeRelease, releaseManifest } from "./mobile-release/release.js";
|
|
124
129
|
import { mobileReleaseApp } from "./mobile-release/release-app.js";
|
|
125
130
|
import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
|
|
126
|
-
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readSigningKey, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, recordOf, releaseManifest, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
131
|
+
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readSigningKey, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, relayed, releaseManifest, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/jobs/run-job.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "../notifications/send-input.js";
|
|
2
2
|
import "./job-types.js";
|
|
3
|
-
import "pg-boss";
|
|
4
3
|
import { Db } from "@protobase/query";
|
|
4
|
+
import "pg-boss";
|
|
5
5
|
//#region src/jobs/run-job.d.ts
|
|
6
6
|
/** Thrown from a job's `run`, it fails the job now: the attempts it has left are not used. */
|
|
7
7
|
export declare class FatalJobError extends Error {
|
package/dist/list-photos.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { badRequest } from "./problem.js";
|
|
2
2
|
import { checkedSort } from "./filter-input.js";
|
|
3
3
|
import { requestAccess } from "./request-access.js";
|
|
4
|
-
import {
|
|
4
|
+
import { readScope } from "./read-scope.js";
|
|
5
5
|
import { exposeRow } from "./rows.js";
|
|
6
|
+
import { readTransaction } from "./transactions.js";
|
|
6
7
|
import { presentFiles } from "./files/present.js";
|
|
7
|
-
import { readScope } from "./read-scope.js";
|
|
8
8
|
import { cardPhotosErrors } from "./view-card-photos.js";
|
|
9
9
|
import { cardPhotoLimit } from "@protobase/schema/internal";
|
|
10
10
|
import { checkFilter, createWhere } from "@protobase/schema";
|
package/dist/list-service.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { checkedFilter, checkedSort } from "./filter-input.js";
|
|
2
2
|
import { requestAccess, requireOperation } from "./request-access.js";
|
|
3
|
+
import { readScope } from "./read-scope.js";
|
|
4
|
+
import { exposeRow, pickFields } from "./rows.js";
|
|
5
|
+
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
3
6
|
import { readTransaction } from "./transactions.js";
|
|
7
|
+
import { deletedRows, listParams, pageSizeOf, parseParams, seekParams, selectedFields } from "./params.js";
|
|
4
8
|
import { etagAlias } from "./sql-parts.js";
|
|
5
9
|
import { etagOf } from "./etag.js";
|
|
6
|
-
import { exposeRow, pickFields } from "./rows.js";
|
|
7
10
|
import { presentFiles } from "./files/present.js";
|
|
8
|
-
import { deletedRows, listParams, pageSizeOf, parseParams, seekParams, selectedFields } from "./params.js";
|
|
9
|
-
import { readScope } from "./read-scope.js";
|
|
10
11
|
import { rowPermissions } from "./row-permissions.js";
|
|
11
|
-
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
12
12
|
import { photosRequest, recordPhotos } from "./list-photos.js";
|
|
13
13
|
import { countUpTo, estimateCount, listQuery, seekToPosition } from "@protobase/query";
|
|
14
14
|
//#region src/list-service.ts
|
|
@@ -102,11 +102,11 @@ const listRecords = async (deps, entry, session, input, maxFilterLength, jump =
|
|
|
102
102
|
next_page_token: result.page.nextCursor ?? "",
|
|
103
103
|
...position !== void 0 && { prev_page_token: result.page.prevCursor ?? "" },
|
|
104
104
|
total_size_estimate: result.estimate,
|
|
105
|
-
...result.counted !== void 0 && result.counted <=
|
|
105
|
+
...result.counted !== void 0 && result.counted <= 1e4 && { total_size: result.counted },
|
|
106
106
|
...result.labels && { labels: result.labels },
|
|
107
107
|
...included && { photos: included.photos }
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
110
|
};
|
|
111
111
|
//#endregion
|
|
112
|
-
export { listRecords };
|
|
112
|
+
export { exactCountLimit, listRecords };
|
package/dist/map-service.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { checkedFilter } from "./filter-input.js";
|
|
2
2
|
import { requestAccess, requireOperation } from "./request-access.js";
|
|
3
|
-
import { readTransaction } from "./transactions.js";
|
|
4
|
-
import { parseParams, selectedFields } from "./params.js";
|
|
5
3
|
import { readScope } from "./read-scope.js";
|
|
6
4
|
import { labelRequests } from "./related-labels.js";
|
|
5
|
+
import { readTransaction } from "./transactions.js";
|
|
6
|
+
import { parseParams, selectedFields } from "./params.js";
|
|
7
7
|
import { photosRequest } from "./list-photos.js";
|
|
8
8
|
import { listRecords } from "./list-service.js";
|
|
9
9
|
import { z } from "zod";
|
|
@@ -5,9 +5,12 @@ import { Meta } from "../meta.js";
|
|
|
5
5
|
import { Locale } from "@protobase/schema/internal";
|
|
6
6
|
//#region src/mcp/mcp-context.d.ts
|
|
7
7
|
/**
|
|
8
|
-
* What tools get over MCP for `session`: its records and the resources it may read, under its access
|
|
9
|
-
* `locale`, the language its approvals are asked in.
|
|
10
|
-
* call its own `approve`.
|
|
8
|
+
* What tools get over MCP for `session`: its records, their comments and the resources it may read, under its access
|
|
9
|
+
* rules, and `locale`, the language its approvals are asked in. `requestURL` is the MCP request's. Nothing shows beside
|
|
10
|
+
* a tool's result, and `mcpServer` gives each call its own `approve`.
|
|
11
11
|
*/
|
|
12
|
-
export declare const mcpToolContext: (deps: Deps, meta: Pick<Meta, "forCaller">, session: Session, locale
|
|
12
|
+
export declare const mcpToolContext: (deps: Deps, meta: Pick<Meta, "forCaller">, session: Session, { locale, requestURL }: {
|
|
13
|
+
locale?: Locale;
|
|
14
|
+
requestURL: string;
|
|
15
|
+
}) => Omit<ToolContext, "approve">;
|
|
13
16
|
//#endregion
|
package/dist/mcp/mcp-context.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { assistantRecords } from "../assistant/records.js";
|
|
2
2
|
import { readableResources } from "../assistant/readable-resources.js";
|
|
3
|
+
import { toolComments } from "../comments/tool-comments.js";
|
|
3
4
|
//#region src/mcp/mcp-context.ts
|
|
4
5
|
/**
|
|
5
|
-
* What tools get over MCP for `session`: its records and the resources it may read, under its access
|
|
6
|
-
* `locale`, the language its approvals are asked in.
|
|
7
|
-
* call its own `approve`.
|
|
6
|
+
* What tools get over MCP for `session`: its records, their comments and the resources it may read, under its access
|
|
7
|
+
* rules, and `locale`, the language its approvals are asked in. `requestURL` is the MCP request's. Nothing shows beside
|
|
8
|
+
* a tool's result, and `mcpServer` gives each call its own `approve`.
|
|
8
9
|
*/
|
|
9
|
-
const mcpToolContext = (deps, meta, session, locale) => ({
|
|
10
|
+
const mcpToolContext = (deps, meta, session, { locale, requestURL }) => ({
|
|
10
11
|
session,
|
|
11
12
|
...locale && { locale },
|
|
12
13
|
records: assistantRecords(deps, session),
|
|
14
|
+
comments: toolComments(deps, session, requestURL),
|
|
13
15
|
resources: () => readableResources(meta, session),
|
|
14
16
|
show: () => {}
|
|
15
17
|
});
|
package/dist/mcp/mcp-server.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { AssistantTool, ToolContext } from "../assistant/tools.js";
|
|
2
2
|
import { McpApprovals } from "./mcp-approval-store.js";
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
|
+
import { Icon, McpServer } from "@modelcontextprotocol/server";
|
|
4
4
|
//#region src/mcp/mcp-server.d.ts
|
|
5
5
|
export type McpServerInput = {
|
|
6
|
-
/** The name, version and
|
|
6
|
+
/** The name, version, title and icons the server tells clients when they connect, each icon an absolute URL. */
|
|
7
7
|
info: {
|
|
8
8
|
name: string;
|
|
9
9
|
version: string;
|
|
10
10
|
title?: string;
|
|
11
|
+
icons?: Icon[];
|
|
11
12
|
};
|
|
12
13
|
tools: AssistantTool[];
|
|
13
14
|
/** What each tool gets as its context, for the caller of the request; `approve` comes from each call's own answers. */
|
package/dist/mcp/mcp-tools.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export type McpToolsInput = {
|
|
|
13
13
|
* `rolesWithoutApproval`.
|
|
14
14
|
*/
|
|
15
15
|
writes: McpWrites;
|
|
16
|
+
/**
|
|
17
|
+
* The resources whose records take comments: offers `list_comments` on them, and `add_comment` with writes on, on
|
|
18
|
+
* those of them that `writes` lists when it lists some. None by default.
|
|
19
|
+
*/
|
|
20
|
+
commented?: string[];
|
|
16
21
|
};
|
|
17
|
-
export declare const mcpTools: ({ db, sql, writes }: McpToolsInput) => (session: Session) => AssistantTool[];
|
|
22
|
+
export declare const mcpTools: ({ db, sql, writes, commented }: McpToolsInput) => (session: Session) => AssistantTool[];
|
|
18
23
|
//#endregion
|
package/dist/mcp/mcp-tools.js
CHANGED
|
@@ -3,19 +3,23 @@ import { readOnlyQueryTool } from "../assistant/query-tools.js";
|
|
|
3
3
|
import { readRecordTools } from "../assistant/record-tools.js";
|
|
4
4
|
import { writeRecordTools } from "../assistant/write-record-tools.js";
|
|
5
5
|
import { batchWriteTool } from "../assistant/batch-write-tool.js";
|
|
6
|
+
import { commentTools } from "../assistant/comment-tools.js";
|
|
6
7
|
//#region src/mcp/mcp-tools.ts
|
|
7
8
|
/**
|
|
8
|
-
* The default MCP tools for a caller: the record tools, the write tools when `writes` is on,
|
|
9
|
-
* is on and the caller sees the assistant.
|
|
9
|
+
* The default MCP tools for a caller: the record tools, the write tools when `writes` is on, the comment tools when a
|
|
10
|
+
* resource takes comments, and read-only SQL when `sql` is on and the caller sees the assistant.
|
|
10
11
|
*/
|
|
11
12
|
const writeTools = (input) => [...writeRecordTools(input), batchWriteTool(input)];
|
|
12
|
-
const mcpTools = ({ db, sql, writes }) => {
|
|
13
|
+
const mcpTools = ({ db, sql, writes, commented = [] }) => {
|
|
13
14
|
const read = readRecordTools();
|
|
14
|
-
const
|
|
15
|
+
const limits = writes === false ? false : writes === true || !writes.resources ? {} : { resources: writes.resources };
|
|
16
|
+
const write = limits === false ? [] : writeTools(limits);
|
|
17
|
+
const comments = commentTools({ commented }, limits);
|
|
15
18
|
const query = sql ? [readOnlyQueryTool(db)] : [];
|
|
16
19
|
return (session) => [
|
|
17
20
|
...read,
|
|
18
21
|
...write,
|
|
22
|
+
...comments,
|
|
19
23
|
...seesAssistant(session.user.roles) ? query : []
|
|
20
24
|
];
|
|
21
25
|
};
|
package/dist/package.js
CHANGED
package/dist/read-service.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { notFound } from "./problem.js";
|
|
2
2
|
import { requestAccess, requireOperation, visibleFields, writableFields } from "./request-access.js";
|
|
3
|
-
import { readTransaction } from "./transactions.js";
|
|
4
3
|
import { pickFields } from "./rows.js";
|
|
4
|
+
import { readTransaction } from "./transactions.js";
|
|
5
|
+
import { deletedRows } from "./params.js";
|
|
5
6
|
import { fetchReadable } from "./fetch-readable.js";
|
|
6
7
|
import { presentFiles } from "./files/present.js";
|
|
7
|
-
import { deletedRows } from "./params.js";
|
|
8
8
|
import { rowPermissions } from "./row-permissions.js";
|
|
9
9
|
//#region src/read-service.ts
|
|
10
10
|
/**
|
|
@@ -17,7 +17,7 @@ const parsedJson = async (c) => {
|
|
|
17
17
|
/**
|
|
18
18
|
* `POST /api/broadcast` and `POST /api/broadcast/:topic/events/:event`, Supabase Realtime's broadcast API, which
|
|
19
19
|
* supabase-js's `send()` on a channel that has not joined and `httpSend()` call: they send to the channels joined on a
|
|
20
|
-
* topic
|
|
20
|
+
* topic, as a broadcast sent on the socket, under the same topic rate. A request is admitted by the
|
|
21
21
|
* app's key and the token in its bearer, as the socket is, and its body is at most a socket's frame. A private message
|
|
22
22
|
* reaches private channels only, and only when the caller may send broadcasts on its topic by the policies on
|
|
23
23
|
* `realtime.messages`: the batch drops one it may not, as Supabase does, and the single message is refused with 403.
|
|
@@ -47,7 +47,7 @@ const broadcastApi = ({ caller, broadcasts, access, report }) => {
|
|
|
47
47
|
app.post("/", admitted, limited, async (c) => {
|
|
48
48
|
if (typeOf(c) !== "application/json") throw broadcastProblem(415, "json-only", "Unsupported Media Type", "Send the messages as application/json");
|
|
49
49
|
const publications = await allowed(c.var.caller, batchOf(await parsedJson(c)));
|
|
50
|
-
if (!broadcasts.publish(publications)) throw tooFast();
|
|
50
|
+
if (!await broadcasts.publish(publications)) throw tooFast();
|
|
51
51
|
return c.body(null, 202);
|
|
52
52
|
});
|
|
53
53
|
app.post("/:topic/events/:event", admitted, limited, async (c) => {
|
|
@@ -57,7 +57,7 @@ const broadcastApi = ({ caller, broadcasts, access, report }) => {
|
|
|
57
57
|
if (type === "application/json" && (typeof payload !== "object" || payload === null || Array.isArray(payload))) throw broadcastProblem(422, "invalid-broadcast", "Unprocessable Content", "Payload must be a JSON object");
|
|
58
58
|
const publication = publicationOf(c.req.param("topic"), c.req.param("event"), payload, c.req.query("private") === "true");
|
|
59
59
|
if ((await allowed(c.var.caller, [publication])).length === 0) throw broadcastProblem(403, "unauthorized", "Forbidden", "Unauthorized");
|
|
60
|
-
if (!broadcasts.publish([publication])) throw tooFast();
|
|
60
|
+
if (!await broadcasts.publish([publication])) throw tooFast();
|
|
61
61
|
return c.body(null, 202);
|
|
62
62
|
});
|
|
63
63
|
return app;
|
|
@@ -46,25 +46,33 @@ const decodeBroadcastPush = (frame) => {
|
|
|
46
46
|
const fitsFrame = (text) => encoder.encode(text).length <= 255;
|
|
47
47
|
/**
|
|
48
48
|
* A broadcast as the binary frame realtime-js reads: kind, topic, user event and metadata lengths, encoding, fields,
|
|
49
|
-
* payload. A payload is JSON, or the bytes of a binary broadcast.
|
|
49
|
+
* metadata as JSON, payload. A payload is JSON, or the bytes of a binary broadcast.
|
|
50
50
|
*/
|
|
51
|
-
const encodeBroadcast = (topic, event, payload) => {
|
|
51
|
+
const encodeBroadcast = (topic, event, payload, meta) => {
|
|
52
52
|
const topicBytes = encoder.encode(topic);
|
|
53
53
|
const eventBytes = encoder.encode(event);
|
|
54
|
+
const metaBytes = encoder.encode(meta === void 0 ? "" : JSON.stringify(meta));
|
|
54
55
|
const binary = payload instanceof ArrayBuffer;
|
|
55
56
|
const body = binary ? new Uint8Array(payload) : encoder.encode(JSON.stringify(payload ?? {}));
|
|
56
|
-
if (topicBytes.length > 255 || eventBytes.length > 255) throw new Error("A broadcast topic and
|
|
57
|
-
const frame = new Uint8Array(5 + topicBytes.length + eventBytes.length + body.length);
|
|
57
|
+
if (topicBytes.length > 255 || eventBytes.length > 255 || metaBytes.length > 255) throw new Error("A broadcast topic, event and metadata are at most 255 bytes each");
|
|
58
|
+
const frame = new Uint8Array(5 + topicBytes.length + eventBytes.length + metaBytes.length + body.length);
|
|
58
59
|
frame.set([
|
|
59
60
|
userBroadcast,
|
|
60
61
|
topicBytes.length,
|
|
61
62
|
eventBytes.length,
|
|
62
|
-
|
|
63
|
+
metaBytes.length,
|
|
63
64
|
binary ? binaryEncoding : jsonEncoding
|
|
64
65
|
]);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
let offset = 5;
|
|
67
|
+
for (const part of [
|
|
68
|
+
topicBytes,
|
|
69
|
+
eventBytes,
|
|
70
|
+
metaBytes,
|
|
71
|
+
body
|
|
72
|
+
]) {
|
|
73
|
+
frame.set(part, offset);
|
|
74
|
+
offset += part.length;
|
|
75
|
+
}
|
|
68
76
|
return frame.buffer;
|
|
69
77
|
};
|
|
70
78
|
//#endregion
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { realtimeLimits } from "./realtime-limits.js";
|
|
2
1
|
import { fixedWindows } from "./fixed-window.js";
|
|
2
|
+
import { realtimeLimits } from "./realtime-limits.js";
|
|
3
3
|
//#region src/realtime/broadcast-hub.ts
|
|
4
4
|
/**
|
|
5
|
-
* The channels of a process by topic, which hear each other's broadcasts in-process
|
|
6
|
-
*
|
|
5
|
+
* The channels of a process by topic, which hear each other's broadcasts in-process; with several processes,
|
|
6
|
+
* `relayedHubs` carries them between them through this hub's `deliver`. Private and public channels of a topic do not hear each other, as on
|
|
7
7
|
* Supabase, so a public channel cannot reach past the policies of a private one. A topic takes `realtimeLimits.topicBroadcastsPerSecond`, whoever sends
|
|
8
8
|
* them; its window is kept here, so a topic left empty and joined again does not start a new one.
|
|
9
9
|
*/
|
|
@@ -12,6 +12,10 @@ const broadcastHub = ({ now = () => Date.now() } = {}) => {
|
|
|
12
12
|
const privateTopics = /* @__PURE__ */ new Map();
|
|
13
13
|
const topicsOf = (isPrivate) => isPrivate ? privateTopics : publicTopics;
|
|
14
14
|
const rate = fixedWindows(realtimeLimits.topicBroadcastsPerSecond, 1e3, now);
|
|
15
|
+
/** Sends a broadcast to the topic's members, whatever the rate, but not to `from` unless it hears its own. */
|
|
16
|
+
const deliver = (publication, from) => {
|
|
17
|
+
for (const member of topicsOf(publication.private).get(publication.topic) ?? []) if (member !== from || member.self) member.receive(publication.event, publication.payload, publication.meta);
|
|
18
|
+
};
|
|
15
19
|
return {
|
|
16
20
|
/** Puts the member on the topic until the returned function takes it off. */
|
|
17
21
|
join: (topic, member) => {
|
|
@@ -29,7 +33,12 @@ const broadcastHub = ({ now = () => Date.now() } = {}) => {
|
|
|
29
33
|
*/
|
|
30
34
|
send: (topic, from, event, payload) => {
|
|
31
35
|
if (!rate(topic)) return false;
|
|
32
|
-
|
|
36
|
+
deliver({
|
|
37
|
+
topic,
|
|
38
|
+
event,
|
|
39
|
+
payload,
|
|
40
|
+
private: from.private
|
|
41
|
+
}, from);
|
|
33
42
|
return true;
|
|
34
43
|
},
|
|
35
44
|
/**
|
|
@@ -40,9 +49,10 @@ const broadcastHub = ({ now = () => Date.now() } = {}) => {
|
|
|
40
49
|
const takes = /* @__PURE__ */ new Map();
|
|
41
50
|
for (const { topic } of publications) takes.set(topic, (takes.get(topic) ?? 0) + 1);
|
|
42
51
|
if (!rate.all(takes)) return false;
|
|
43
|
-
for (const publication of publications)
|
|
52
|
+
for (const publication of publications) deliver(publication);
|
|
44
53
|
return true;
|
|
45
|
-
}
|
|
54
|
+
},
|
|
55
|
+
deliver
|
|
46
56
|
};
|
|
47
57
|
};
|
|
48
58
|
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
|
-
import { fitsFrame } from "./broadcast-frame.js";
|
|
3
2
|
import { realtimeLimits } from "./realtime-limits.js";
|
|
3
|
+
import { fitsFrame } from "./broadcast-frame.js";
|
|
4
4
|
//#region src/realtime/broadcast-request.ts
|
|
5
5
|
/** A refused broadcast request; `message` too, which realtime-js's `httpSend` reads as the error. */
|
|
6
6
|
const broadcastProblem = (status, slug, title, detail) => new HttpProblem(status, slug, title, detail, { message: detail });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { quoteIdent, tableSql } from "../changes/capture-sql.js";
|
|
2
1
|
import { readTransaction } from "../transactions.js";
|
|
2
|
+
import { quoteIdent, tableSql } from "../changes/capture-sql.js";
|
|
3
3
|
import { callerSettings } from "../rest/caller-settings.js";
|
|
4
4
|
import { filterSql } from "./change-filter.js";
|
|
5
5
|
import { decodeKey, encodeKey } from "@protobase/schema/internal";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isDatabaseError } from "../error-handler.js";
|
|
2
1
|
import { writeTransaction } from "../transactions.js";
|
|
2
|
+
import { isDatabaseError } from "../error-handler.js";
|
|
3
3
|
import { callerSettings } from "../rest/caller-settings.js";
|
|
4
4
|
import { sql } from "kysely";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { writeTransaction } from "../transactions.js";
|
|
2
|
+
import { realtimeLimits } from "./realtime-limits.js";
|
|
3
|
+
import { fitsFrame } from "./broadcast-frame.js";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
import { Buffer } from "node:buffer";
|
|
6
|
+
//#region src/realtime/database-broadcasts.ts
|
|
7
|
+
/** Messages taken in one transaction at most. */
|
|
8
|
+
const batchSize = 1e3;
|
|
9
|
+
/**
|
|
10
|
+
* The message as the channels joined on its topic hear it, with its id as `meta`, as Supabase sends it; `undefined` for
|
|
11
|
+
* one Supabase would skip, or whose topic or event does not fit a frame or whose payload is over a frame.
|
|
12
|
+
*/
|
|
13
|
+
const publicationOfRow = (row) => {
|
|
14
|
+
if (row.topic === null || row.event === null || row.payload === null || row.private === null) return void 0;
|
|
15
|
+
const topic = `realtime:${row.topic}`;
|
|
16
|
+
if (!fitsFrame(topic) || !fitsFrame(row.event)) return void 0;
|
|
17
|
+
if (Buffer.byteLength(JSON.stringify(row.payload)) > realtimeLimits.maxFrameBytes) return void 0;
|
|
18
|
+
return {
|
|
19
|
+
topic,
|
|
20
|
+
event: row.event,
|
|
21
|
+
payload: row.payload,
|
|
22
|
+
private: row.private,
|
|
23
|
+
meta: { id: row.id }
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const takeMessages = (db, statementTimeoutMs) => writeTransaction(db, statementTimeoutMs, async (trx) => {
|
|
27
|
+
const { rows } = await sql`with taken as (
|
|
28
|
+
delete from realtime.messages where id in (
|
|
29
|
+
select id from realtime.messages where extension = 'broadcast' order by inserted_at limit ${batchSize} for update skip locked
|
|
30
|
+
) returning id, topic, event, payload, private, inserted_at
|
|
31
|
+
)
|
|
32
|
+
select id::text as id, topic, event, payload, private from taken order by inserted_at`.execute(trx);
|
|
33
|
+
return rows;
|
|
34
|
+
});
|
|
35
|
+
const tableExists = async (db) => (await sql`select to_regclass('realtime.messages') is not null as found`.execute(db)).rows[0].found;
|
|
36
|
+
/**
|
|
37
|
+
* Supabase's broadcast from the database: `realtime.send()` and `realtime.broadcast_changes()` insert a message into
|
|
38
|
+
* `realtime.messages` in the caller's transaction, so only a committed one is seen, and while a socket of this process
|
|
39
|
+
* is open the table is read every `intervalMs`, its messages taken out of it and sent to the channels joined on their
|
|
40
|
+
* topics, the private or the public ones, under the topic's broadcast rate. A message nobody hears is dropped, as on
|
|
41
|
+
* Supabase: the first read when a socket opens takes those sent while none was. A database without
|
|
42
|
+
* `realtime.messages` sends nothing, and is checked again at each read.
|
|
43
|
+
*/
|
|
44
|
+
const databaseBroadcasts = ({ db, statementTimeoutMs, broadcasts, intervalMs = 1e3, report }) => {
|
|
45
|
+
let followers = 0;
|
|
46
|
+
let running = false;
|
|
47
|
+
const read = async () => {
|
|
48
|
+
if (!await tableExists(db)) return;
|
|
49
|
+
for (;;) {
|
|
50
|
+
const rows = await takeMessages(db, statementTimeoutMs);
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const publication = publicationOfRow(row);
|
|
53
|
+
if (publication) await broadcasts.publish([publication]);
|
|
54
|
+
}
|
|
55
|
+
if (rows.length < batchSize) return;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const tick = async () => {
|
|
59
|
+
if (followers === 0) {
|
|
60
|
+
running = false;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await read().catch(report);
|
|
64
|
+
setTimeout(() => void tick(), intervalMs);
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
/** Reads the table from now on, until the returned function is called; once for each call, however often. */
|
|
68
|
+
follow: () => {
|
|
69
|
+
followers += 1;
|
|
70
|
+
if (!running) {
|
|
71
|
+
running = true;
|
|
72
|
+
tick();
|
|
73
|
+
}
|
|
74
|
+
let followed = true;
|
|
75
|
+
return () => {
|
|
76
|
+
if (!followed) return;
|
|
77
|
+
followed = false;
|
|
78
|
+
followers -= 1;
|
|
79
|
+
};
|
|
80
|
+
} };
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
export { databaseBroadcasts, publicationOfRow };
|
|
@@ -4,58 +4,93 @@ const stateOf = (tracked) => {
|
|
|
4
4
|
for (const [key, meta] of tracked) (state[key] ??= { metas: [] }).metas.push(meta);
|
|
5
5
|
return state;
|
|
6
6
|
};
|
|
7
|
+
const alone = {
|
|
8
|
+
state: () => [],
|
|
9
|
+
changed: () => {},
|
|
10
|
+
ready: async () => {}
|
|
11
|
+
};
|
|
7
12
|
/**
|
|
8
13
|
* The presences of a process by topic, in-process like broadcasts: each joined channel tracks at most one, under its
|
|
9
14
|
* key, and channels that listen hear every join and leave on their topic, their own included, as Phoenix Presence
|
|
10
|
-
* sends them. Tracking again replaces the presence: a leave of the old one and a join of the new. Refs count up
|
|
11
|
-
* in the process. Private and public channels of a topic do not see each other, as with broadcasts.
|
|
15
|
+
* sends them. Tracking again replaces the presence: a leave of the old one and a join of the new. Refs count up after
|
|
16
|
+
* `refPrefix`, unique in the process. Private and public channels of a topic do not see each other, as with broadcasts.
|
|
17
|
+
* With `remote`, a topic's state holds the other processes' presences too, each change goes to them, and `hear` tells
|
|
18
|
+
* the channels theirs.
|
|
12
19
|
*/
|
|
13
|
-
const presenceHub = () => {
|
|
20
|
+
const presenceHub = ({ refPrefix = "", remote = alone } = {}) => {
|
|
14
21
|
const publicTopics = /* @__PURE__ */ new Map();
|
|
15
22
|
const privateTopics = /* @__PURE__ */ new Map();
|
|
16
23
|
let lastRef = 0;
|
|
24
|
+
const topicsOf = (isPrivate) => isPrivate ? privateTopics : publicTopics;
|
|
17
25
|
const tell = (members, diff) => {
|
|
18
26
|
for (const member of members.keys()) if (member.listens) member.receive(diff);
|
|
19
27
|
};
|
|
20
|
-
return {
|
|
21
|
-
|
|
22
|
-
join: (topic, member) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const old = members.get(member);
|
|
28
|
-
if (!old) return;
|
|
29
|
-
members.set(member, void 0);
|
|
30
|
-
tell(members, {
|
|
31
|
-
joins: {},
|
|
32
|
-
leaves: stateOf([[member.key, old]])
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
return {
|
|
36
|
-
/** The topic's presences now, for `presence_state`. */
|
|
37
|
-
state: () => stateOf([...members].flatMap(([{ key }, meta]) => meta ? [[key, meta]] : [])),
|
|
38
|
-
track: (payload) => {
|
|
28
|
+
return {
|
|
29
|
+
/** Puts the member on the topic, with what it may do there until it leaves. */
|
|
30
|
+
join: (topic, member) => {
|
|
31
|
+
const topics = topicsOf(member.private);
|
|
32
|
+
const members = topics.get(topic) ?? /* @__PURE__ */ new Map();
|
|
33
|
+
topics.set(topic, members.set(member, void 0));
|
|
34
|
+
const untrack = () => {
|
|
39
35
|
const old = members.get(member);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
phx_ref: String(++lastRef),
|
|
43
|
-
...old && { phx_ref_prev: old.phx_ref }
|
|
44
|
-
};
|
|
45
|
-
members.set(member, meta);
|
|
36
|
+
if (!old) return;
|
|
37
|
+
members.set(member, void 0);
|
|
46
38
|
tell(members, {
|
|
47
|
-
joins:
|
|
48
|
-
leaves:
|
|
39
|
+
joins: {},
|
|
40
|
+
leaves: stateOf([[member.key, old]])
|
|
41
|
+
});
|
|
42
|
+
remote.changed({
|
|
43
|
+
topic,
|
|
44
|
+
private: member.private,
|
|
45
|
+
key: member.key,
|
|
46
|
+
left: old
|
|
49
47
|
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
/** The topic's presences now, for `presence_state`. */
|
|
51
|
+
state: () => stateOf([...[...members].flatMap(([{ key }, meta]) => meta ? [[key, meta]] : []), ...remote.state(topic, member.private)]),
|
|
52
|
+
track: (payload) => {
|
|
53
|
+
const old = members.get(member);
|
|
54
|
+
const meta = {
|
|
55
|
+
...payload,
|
|
56
|
+
phx_ref: `${refPrefix}${++lastRef}`,
|
|
57
|
+
...old && { phx_ref_prev: old.phx_ref }
|
|
58
|
+
};
|
|
59
|
+
members.set(member, meta);
|
|
60
|
+
tell(members, {
|
|
61
|
+
joins: stateOf([[member.key, meta]]),
|
|
62
|
+
leaves: old ? stateOf([[member.key, old]]) : {}
|
|
63
|
+
});
|
|
64
|
+
remote.changed({
|
|
65
|
+
topic,
|
|
66
|
+
private: member.private,
|
|
67
|
+
key: member.key,
|
|
68
|
+
joined: meta,
|
|
69
|
+
...old && { left: old }
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
untrack,
|
|
73
|
+
leave: () => {
|
|
74
|
+
untrack();
|
|
75
|
+
members.delete(member);
|
|
76
|
+
if (members.size === 0 && topics.get(topic) === members) topics.delete(topic);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
/** Tells the topic's listening channels a diff of another process's presences. */
|
|
81
|
+
hear: (topic, isPrivate, diff) => {
|
|
82
|
+
const members = topicsOf(isPrivate).get(topic);
|
|
83
|
+
if (members) tell(members, diff);
|
|
84
|
+
},
|
|
85
|
+
/** Every presence this process's channels track. */
|
|
86
|
+
tracked: () => [true, false].flatMap((isPrivate) => [...topicsOf(isPrivate)].flatMap(([topic, members]) => [...members].flatMap(([{ key }, meta]) => meta ? [{
|
|
87
|
+
topic,
|
|
88
|
+
private: isPrivate,
|
|
89
|
+
key,
|
|
90
|
+
meta
|
|
91
|
+
}] : []))),
|
|
92
|
+
ready: remote.ready
|
|
93
|
+
};
|
|
59
94
|
};
|
|
60
95
|
//#endregion
|
|
61
96
|
export { presenceHub };
|