From 15afb8f1b31e1e2ca1e0f0692e522f6f3f72ca88 Mon Sep 17 00:00:00 2001 From: Stephen Niedzielski Date: Wed, 6 Mar 2024 10:20:17 -0700 Subject: [PATCH] [DX-5796] Support remote execution Add support for forwarding circuit breaks to remote runtimes. --- package-lock.json | 49 + package.json | 3 +- src/bundler/linker.ts | 2598 +++++++++++++++++- src/elements/play-editor/play-editor.test.ts | 50 +- src/elements/play-pen-footer.ts | 25 +- src/elements/play-pen-header.ts | 30 +- src/elements/play-pen/play-pen.ts | 111 +- src/elements/play-preview.test.ts | 7 - src/elements/play-preview.ts | 42 +- src/elements/play-settings-dialog.test.ts | 7 + src/elements/play-settings-dialog.ts | 215 ++ src/index.ts | 2 +- src/runtime/bundle-store.ts | 15 + src/runtime/remote-app.ts | 124 + src/{types => storage}/pen-save.test.ts | 0 src/{types => storage}/pen-save.ts | 0 src/storage/settings-save.ts | 52 + src/utils/compute-util.ts | 5 + 18 files changed, 3274 insertions(+), 61 deletions(-) delete mode 100644 src/elements/play-preview.test.ts create mode 100644 src/elements/play-settings-dialog.test.ts create mode 100644 src/elements/play-settings-dialog.ts create mode 100644 src/runtime/bundle-store.ts create mode 100644 src/runtime/remote-app.ts rename src/{types => storage}/pen-save.test.ts (100%) rename src/{types => storage}/pen-save.ts (100%) create mode 100644 src/storage/settings-save.ts create mode 100644 src/utils/compute-util.ts diff --git a/package-lock.json b/package-lock.json index 6534f0d..914c81d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "lit": "3.1.2", "lit-analyzer": "2.0.3", "lz-string": "1.5.0", + "nice-grpc-web": "3.3.3", "prettier": "3.2.5", "typescript": "5.4.3", "vitest": "1.4.0" @@ -2887,6 +2888,12 @@ "node": ">= 8" } }, + "node_modules/abort-controller-x": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz", + "integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5201,6 +5208,15 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -5258,6 +5274,12 @@ "node": ">=8" } }, + "node_modules/js-base64": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "dev": true + }, "node_modules/js-tokens": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", @@ -6016,6 +6038,27 @@ "node": ">= 0.4.0" } }, + "node_modules/nice-grpc-common": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz", + "integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==", + "dev": true, + "dependencies": { + "ts-error": "^1.0.6" + } + }, + "node_modules/nice-grpc-web": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nice-grpc-web/-/nice-grpc-web-3.3.3.tgz", + "integrity": "sha512-sVz8n3tRwk8Iy8Lppek0qD1HFGexvy0915/uiXfQvOTUDHCsTJqcIEjmSQWLO6FZtZMwVMtPXrs9WZMNDrkcyQ==", + "dev": true, + "dependencies": { + "abort-controller-x": "^0.4.0", + "isomorphic-ws": "^5.0.0", + "js-base64": "^3.7.2", + "nice-grpc-common": "^2.0.2" + } + }, "node_modules/node-abi": { "version": "3.56.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz", @@ -7796,6 +7839,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz", + "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==", + "dev": true + }, "node_modules/ts-simple-type": { "version": "2.0.0-next.0", "resolved": "https://registry.npmjs.org/ts-simple-type/-/ts-simple-type-2.0.0-next.0.tgz", diff --git a/package.json b/package.json index b85807a..d1ded9b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "lit": "3.1.2", "lit-analyzer": "2.0.3", "lz-string": "1.5.0", + "nice-grpc-web": "3.3.3", "prettier": "3.2.5", "typescript": "5.4.3", "vitest": "1.4.0" @@ -54,7 +55,7 @@ "gzip": "3.5 KB" }, "dist/play-pen.js": { - "gzip": "3.7 MB" + "gzip": "4.0 MB" } }, "typesVersions": { diff --git a/src/bundler/linker.ts b/src/bundler/linker.ts index 6f12e53..6e303d8 100644 --- a/src/bundler/linker.ts +++ b/src/bundler/linker.ts @@ -1,12 +1,29 @@ import type {LinkedBundle, SerializableServiceDefinition} from '@devvit/protos' -export function link(es: string): LinkedBundle { - return {assets: {}, code: es, hostname: '', provides: provides(), uses: []} +/** + * @arg es JavaScript + * @arg hostname Arbitrary but something unique to the window like + * hello-world.local may allow concurrent sessions with the + * remote. + */ +export function link(es: string, hostname: string): LinkedBundle { + return { + actor: {name: 'pen', owner: 'play', version: '0.0.0.0'}, + assets: {}, + code: es, + hostname, + provides: provides(), + uses: uses() + } } function provides(): SerializableServiceDefinition[] { // Assume the app provides CustomPost and UIEventHandler services to avoid an - // evaluation phase. + // evaluation phase. These are generated like: + // devvit new foo --template=custom-post + // cd foo + // devvit actor bundle main + // node -pe 'JSON.stringify(JSON.parse(require("fs").readFileSync(0, "utf8")).dependencies.provides, null, 2)' < dist/main.bundle.json return [ { fullName: 'devvit.reddit.custom_post.v1alpha.CustomPost', @@ -58,3 +75,2578 @@ function provides(): SerializableServiceDefinition[] { } ] } + +function uses(): LinkedBundle[] { + // Assume the app uses everything (`Devvit.configure({...})`). play doesn't + // have an evaluation phase. See provides() for generation notes. + return [ + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'http.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.http.HTTP', + methods: [ + { + fullName: '/devvit.plugin.http.HTTP/Fetch', + name: 'Fetch', + requestType: 'devvit.plugin.http.FetchRequest', + responseType: 'devvit.plugin.http.FetchResponse', + requestStream: false, + responseStream: false + } + ], + name: 'HTTP', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'kvstore.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.kvstore.KVStore', + methods: [ + { + fullName: '/devvit.plugin.kvstore.KVStore/Put', + name: 'Put', + requestType: 'devvit.plugin.kvstore.MessageSet', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.kvstore.KVStore/Get', + name: 'Get', + requestType: 'devvit.plugin.kvstore.KeySet', + responseType: 'devvit.plugin.kvstore.MessageSet', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.kvstore.KVStore/Del', + name: 'Del', + requestType: 'devvit.plugin.kvstore.KeySet', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.kvstore.KVStore/List', + name: 'List', + requestType: 'devvit.plugin.kvstore.ListFilter', + responseType: 'devvit.plugin.kvstore.KeySet', + requestStream: false, + responseStream: false + } + ], + name: 'KVStore', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'redisapi.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.redis.RedisAPI', + methods: [ + { + fullName: '/devvit.plugin.redis.RedisAPI/Get', + name: 'Get', + requestType: 'devvit.plugin.redis.KeyRequest', + responseType: 'google.protobuf.StringValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Set', + name: 'Set', + requestType: 'devvit.plugin.redis.SetRequest', + responseType: 'google.protobuf.StringValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Del', + name: 'Del', + requestType: 'devvit.plugin.redis.KeysRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Type', + name: 'Type', + requestType: 'devvit.plugin.redis.KeyRequest', + responseType: 'google.protobuf.StringValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/IncrBy', + name: 'IncrBy', + requestType: 'devvit.plugin.redis.IncrByRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/HSet', + name: 'HSet', + requestType: 'devvit.plugin.redis.HSetRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/HGet', + name: 'HGet', + requestType: 'devvit.plugin.redis.HGetRequest', + responseType: 'google.protobuf.StringValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/HGetAll', + name: 'HGetAll', + requestType: 'devvit.plugin.redis.KeyRequest', + responseType: 'devvit.plugin.redis.RedisFieldValues', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/HDel', + name: 'HDel', + requestType: 'devvit.plugin.redis.HDelRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/HScan', + name: 'HScan', + requestType: 'devvit.plugin.redis.HScanRequest', + responseType: 'devvit.plugin.redis.HScanResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/HKeys', + name: 'HKeys', + requestType: 'devvit.plugin.redis.KeyRequest', + responseType: 'devvit.plugin.redis.KeysResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/HIncrBy', + name: 'HIncrBy', + requestType: 'devvit.plugin.redis.HIncrByRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Multi', + name: 'Multi', + requestType: 'devvit.plugin.redis.TransactionId', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Exec', + name: 'Exec', + requestType: 'devvit.plugin.redis.TransactionId', + responseType: 'devvit.plugin.redis.TransactionResponses', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Discard', + name: 'Discard', + requestType: 'devvit.plugin.redis.TransactionId', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Watch', + name: 'Watch', + requestType: 'devvit.plugin.redis.WatchRequest', + responseType: 'devvit.plugin.redis.TransactionId', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Unwatch', + name: 'Unwatch', + requestType: 'devvit.plugin.redis.TransactionId', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/GetRange', + name: 'GetRange', + requestType: 'devvit.plugin.redis.KeyRangeRequest', + responseType: 'google.protobuf.StringValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/SetRange', + name: 'SetRange', + requestType: 'devvit.plugin.redis.SetRangeRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Strlen', + name: 'Strlen', + requestType: 'devvit.plugin.redis.KeyRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/MGet', + name: 'MGet', + requestType: 'devvit.plugin.redis.KeysRequest', + responseType: 'devvit.plugin.redis.RedisValues', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/MSet', + name: 'MSet', + requestType: 'devvit.plugin.redis.KeyValuesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/Expire', + name: 'Expire', + requestType: 'devvit.plugin.redis.ExpireRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ExpireTime', + name: 'ExpireTime', + requestType: 'devvit.plugin.redis.KeyRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZAdd', + name: 'ZAdd', + requestType: 'devvit.plugin.redis.ZAddRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZCard', + name: 'ZCard', + requestType: 'devvit.plugin.redis.KeyRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZRange', + name: 'ZRange', + requestType: 'devvit.plugin.redis.ZRangeRequest', + responseType: 'devvit.plugin.redis.ZMembers', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZRem', + name: 'ZRem', + requestType: 'devvit.plugin.redis.ZRemRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZRemRangeByLex', + name: 'ZRemRangeByLex', + requestType: 'devvit.plugin.redis.ZRemRangeByLexRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZRemRangeByRank', + name: 'ZRemRangeByRank', + requestType: 'devvit.plugin.redis.ZRemRangeByRankRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZRemRangeByScore', + name: 'ZRemRangeByScore', + requestType: 'devvit.plugin.redis.ZRemRangeByScoreRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZScore', + name: 'ZScore', + requestType: 'devvit.plugin.redis.ZScoreRequest', + responseType: 'google.protobuf.DoubleValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZRank', + name: 'ZRank', + requestType: 'devvit.plugin.redis.ZRankRequest', + responseType: 'google.protobuf.Int64Value', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZIncrBy', + name: 'ZIncrBy', + requestType: 'devvit.plugin.redis.ZIncrByRequest', + responseType: 'google.protobuf.DoubleValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redis.RedisAPI/ZScan', + name: 'ZScan', + requestType: 'devvit.plugin.redis.ZScanRequest', + responseType: 'devvit.plugin.redis.ZScanResponse', + requestStream: false, + responseStream: false + } + ], + name: 'RedisAPI', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'mediaservice.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.media.MediaService', + methods: [ + { + fullName: '/devvit.plugin.media.MediaService/Upload', + name: 'Upload', + requestType: 'devvit.plugin.media.MediaUploadRequest', + responseType: 'devvit.plugin.media.MediaUploadResponse', + requestStream: false, + responseStream: false + } + ], + name: 'MediaService', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'modlog.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.modlog.Modlog', + methods: [ + { + fullName: '/devvit.plugin.modlog.Modlog/Add', + name: 'Add', + requestType: 'devvit.plugin.modlog.ModlogRequest', + responseType: 'devvit.plugin.modlog.ModlogResponse', + requestStream: false, + responseStream: false + } + ], + name: 'Modlog', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'flair.flair.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.flair.Flair', + methods: [ + { + fullName: + '/devvit.plugin.redditapi.flair.Flair/ClearFlairTemplates', + name: 'ClearFlairTemplates', + requestType: + 'devvit.plugin.redditapi.flair.ClearFlairTemplatesRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/DeleteFlair', + name: 'DeleteFlair', + requestType: 'devvit.plugin.redditapi.flair.DeleteFlairRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.flair.Flair/DeleteFlairTemplate', + name: 'DeleteFlairTemplate', + requestType: + 'devvit.plugin.redditapi.flair.DeleteFlairTemplateRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/Flair', + name: 'Flair', + requestType: 'devvit.plugin.redditapi.flair.FlairRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.flair.Flair/FlairTemplateOrder', + name: 'FlairTemplateOrder', + requestType: + 'devvit.plugin.redditapi.flair.FlairTemplateOrderRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/FlairConfig', + name: 'FlairConfig', + requestType: 'devvit.plugin.redditapi.flair.FlairConfigRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/FlairCsv', + name: 'FlairCsv', + requestType: 'devvit.plugin.redditapi.flair.FlairCsvRequest', + responseType: 'devvit.plugin.redditapi.flair.FlairCsvResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/FlairList', + name: 'FlairList', + requestType: 'devvit.plugin.redditapi.flair.FlairListRequest', + responseType: 'devvit.plugin.redditapi.flair.FlairListResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/FlairSelector', + name: 'FlairSelector', + requestType: 'devvit.plugin.redditapi.flair.FlairSelectorRequest', + responseType: + 'devvit.plugin.redditapi.flair.FlairSelectorResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/FlairTemplate', + name: 'FlairTemplate', + requestType: 'devvit.plugin.redditapi.flair.FlairTemplateRequest', + responseType: 'devvit.plugin.redditapi.flair.FlairObject', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/LinkFlair', + name: 'LinkFlair', + requestType: 'devvit.plugin.redditapi.flair.LinkFlairRequest', + responseType: 'devvit.plugin.redditapi.flair.FlairArray', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/SelectFlair', + name: 'SelectFlair', + requestType: 'devvit.plugin.redditapi.flair.SelectFlairRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/SetFlairEnabled', + name: 'SetFlairEnabled', + requestType: + 'devvit.plugin.redditapi.flair.SetFlairEnabledRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.flair.Flair/UserFlair', + name: 'UserFlair', + requestType: 'devvit.plugin.redditapi.flair.LinkFlairRequest', + responseType: 'devvit.plugin.redditapi.flair.FlairArray', + requestStream: false, + responseStream: false + } + ], + name: 'Flair', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'graphql.graphql.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.graphql.GraphQL', + methods: [ + { + fullName: '/devvit.plugin.redditapi.graphql.GraphQL/Query', + name: 'Query', + requestType: 'devvit.plugin.redditapi.graphql.QueryRequest', + responseType: 'devvit.plugin.redditapi.graphql.QueryResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.graphql.GraphQL/PersistedQuery', + name: 'PersistedQuery', + requestType: + 'devvit.plugin.redditapi.graphql.PersistedQueryRequest', + responseType: 'devvit.plugin.redditapi.graphql.QueryResponse', + requestStream: false, + responseStream: false + } + ], + name: 'GraphQL', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'linksandcomments.linksandcomments.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.linksandcomments.LinksAndComments', + methods: [ + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Comment', + name: 'Comment', + requestType: + 'devvit.plugin.redditapi.linksandcomments.CommentRequest', + responseType: + 'devvit.plugin.redditapi.linksandcomments.JsonWrappedComment', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Del', + name: 'Del', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/EditUserText', + name: 'EditUserText', + requestType: + 'devvit.plugin.redditapi.linksandcomments.CommentRequest', + responseType: + 'devvit.plugin.redditapi.linksandcomments.JsonWrappedComment', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/FollowPost', + name: 'FollowPost', + requestType: + 'devvit.plugin.redditapi.linksandcomments.FollowPostRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Hide', + name: 'Hide', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Info', + name: 'Info', + requestType: + 'devvit.plugin.redditapi.linksandcomments.InfoRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Lock', + name: 'Lock', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/MarkNSFW', + name: 'MarkNSFW', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/MoreChildren', + name: 'MoreChildren', + requestType: + 'devvit.plugin.redditapi.linksandcomments.MoreChildrenRequest', + responseType: + 'devvit.plugin.redditapi.linksandcomments.JsonWrappedComment', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Report', + name: 'Report', + requestType: + 'devvit.plugin.redditapi.linksandcomments.ReportRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/ReportAward', + name: 'ReportAward', + requestType: + 'devvit.plugin.redditapi.linksandcomments.ReportAwardRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Save', + name: 'Save', + requestType: + 'devvit.plugin.redditapi.linksandcomments.SaveRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/SendReplies', + name: 'SendReplies', + requestType: + 'devvit.plugin.redditapi.linksandcomments.SendRepliesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/SetContestMode', + name: 'SetContestMode', + requestType: + 'devvit.plugin.redditapi.linksandcomments.SetContestModeRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/SetSubredditSticky', + name: 'SetSubredditSticky', + requestType: + 'devvit.plugin.redditapi.linksandcomments.SetSubredditStickyRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/SetSuggestedSort', + name: 'SetSuggestedSort', + requestType: + 'devvit.plugin.redditapi.linksandcomments.SetSuggestedSortRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Spoiler', + name: 'Spoiler', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Submit', + name: 'Submit', + requestType: + 'devvit.plugin.redditapi.linksandcomments.SubmitRequest', + responseType: + 'devvit.plugin.redditapi.linksandcomments.SubmitResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Unhide', + name: 'Unhide', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Unlock', + name: 'Unlock', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/UnmarkNSFW', + name: 'UnmarkNSFW', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Unsave', + name: 'Unsave', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Unspoiler', + name: 'Unspoiler', + requestType: + 'devvit.plugin.redditapi.linksandcomments.BasicIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.linksandcomments.LinksAndComments/Vote', + name: 'Vote', + requestType: + 'devvit.plugin.redditapi.linksandcomments.VoteRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + } + ], + name: 'LinksAndComments', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'listings.listings.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.listings.Listings', + methods: [ + { + fullName: '/devvit.plugin.redditapi.listings.Listings/Best', + name: 'Best', + requestType: 'devvit.plugin.redditapi.listings.GetBestRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.listings.Listings/ById', + name: 'ById', + requestType: 'devvit.plugin.redditapi.listings.GetByIdRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.listings.Listings/Comments', + name: 'Comments', + requestType: + 'devvit.plugin.redditapi.listings.GetCommentsRequest', + responseType: 'devvit.plugin.redditapi.listings.ListingResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.listings.Listings/Duplicates', + name: 'Duplicates', + requestType: + 'devvit.plugin.redditapi.listings.GetDuplicatesRequest', + responseType: 'devvit.plugin.redditapi.listings.ListingResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.listings.Listings/Hot', + name: 'Hot', + requestType: 'devvit.plugin.redditapi.listings.GetHotRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.listings.Listings/New', + name: 'New', + requestType: 'devvit.plugin.redditapi.listings.GetNewRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.listings.Listings/Rising', + name: 'Rising', + requestType: 'devvit.plugin.redditapi.listings.GetRisingRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.listings.Listings/Sort', + name: 'Sort', + requestType: 'devvit.plugin.redditapi.listings.GetSortRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + } + ], + name: 'Listings', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'moderation.moderation.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.moderation.Moderation', + methods: [ + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/AboutLog', + name: 'AboutLog', + requestType: 'devvit.plugin.redditapi.moderation.AboutLogRequest', + responseType: + 'devvit.plugin.redditapi.moderation.AboutLogResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/AboutLocation', + name: 'AboutLocation', + requestType: + 'devvit.plugin.redditapi.moderation.AboutLocationRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/AboutReports', + name: 'AboutReports', + requestType: + 'devvit.plugin.redditapi.moderation.AboutLocationRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/AboutSpam', + name: 'AboutSpam', + requestType: + 'devvit.plugin.redditapi.moderation.AboutLocationRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/AboutModqueue', + name: 'AboutModqueue', + requestType: + 'devvit.plugin.redditapi.moderation.AboutLocationRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/AboutUnmoderated', + name: 'AboutUnmoderated', + requestType: + 'devvit.plugin.redditapi.moderation.AboutLocationRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/AboutEdited', + name: 'AboutEdited', + requestType: + 'devvit.plugin.redditapi.moderation.AboutLocationRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/AcceptModeratorInvite', + name: 'AcceptModeratorInvite', + requestType: + 'devvit.plugin.redditapi.moderation.AcceptModeratorInviteRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/Approve', + name: 'Approve', + requestType: + 'devvit.plugin.redditapi.moderation.BasicModerationIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/Distinguish', + name: 'Distinguish', + requestType: + 'devvit.plugin.redditapi.moderation.DistinguishRequest', + responseType: 'devvit.plugin.redditapi.common.JsonRedditObjects', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/IgnoreReports', + name: 'IgnoreReports', + requestType: + 'devvit.plugin.redditapi.moderation.BasicModerationIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/LeaveContributor', + name: 'LeaveContributor', + requestType: + 'devvit.plugin.redditapi.moderation.BasicModerationIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/LeaveModerator', + name: 'LeaveModerator', + requestType: + 'devvit.plugin.redditapi.moderation.BasicModerationIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/MuteMessageAuthor', + name: 'MuteMessageAuthor', + requestType: + 'devvit.plugin.redditapi.moderation.BasicModerationIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.moderation.Moderation/Remove', + name: 'Remove', + requestType: 'devvit.plugin.redditapi.moderation.RemoveRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/ShowComment', + name: 'ShowComment', + requestType: + 'devvit.plugin.redditapi.moderation.BasicModerationIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/SnoozeReports', + name: 'SnoozeReports', + requestType: + 'devvit.plugin.redditapi.moderation.SnoozeReportsRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/UnignoreReports', + name: 'UnignoreReports', + requestType: + 'devvit.plugin.redditapi.moderation.BasicModerationIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/UnmuteMessageAuthor', + name: 'UnmuteMessageAuthor', + requestType: + 'devvit.plugin.redditapi.moderation.BasicModerationIdRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/UnsnoozeReports', + name: 'UnsnoozeReports', + requestType: + 'devvit.plugin.redditapi.moderation.SnoozeReportsRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/UpdateCrowdControlLevel', + name: 'UpdateCrowdControlLevel', + requestType: + 'devvit.plugin.redditapi.moderation.UpdateCrowdControlLevelRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.moderation.Moderation/Stylesheet', + name: 'Stylesheet', + requestType: + 'devvit.plugin.redditapi.moderation.StylesheetRequest', + responseType: 'google.protobuf.StringValue', + requestStream: false, + responseStream: false + } + ], + name: 'Moderation', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'modnote.modnote.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.modnote.ModNote', + methods: [ + { + fullName: '/devvit.plugin.redditapi.modnote.ModNote/GetNotes', + name: 'GetNotes', + requestType: 'devvit.plugin.redditapi.modnote.GetNotesRequest', + responseType: 'devvit.plugin.redditapi.modnote.ModNotesResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.modnote.ModNote/DeleteNotes', + name: 'DeleteNotes', + requestType: 'devvit.plugin.redditapi.modnote.DeleteNotesRequest', + responseType: + 'devvit.plugin.redditapi.modnote.DeleteNotesResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.modnote.ModNote/PostNotes', + name: 'PostNotes', + requestType: 'devvit.plugin.redditapi.modnote.PostNotesRequest', + responseType: + 'devvit.plugin.redditapi.modnote.PostModNotesResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.modnote.ModNote/PostRemovalNote', + name: 'PostRemovalNote', + requestType: + 'devvit.plugin.redditapi.modnote.PostRemovalNoteRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.modnote.ModNote/RecentNotes', + name: 'RecentNotes', + requestType: 'devvit.plugin.redditapi.modnote.RecentNotesRequest', + responseType: 'devvit.plugin.redditapi.modnote.ModNotesResponse', + requestStream: false, + responseStream: false + } + ], + name: 'ModNote', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'newmodmail.newmodmail.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.newmodmail.NewModmail', + methods: [ + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/BulkReadConversations', + name: 'BulkReadConversations', + requestType: + 'devvit.plugin.redditapi.newmodmail.BulkReadConversationsRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.BulkReadConversationsResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/GetConversations', + name: 'GetConversations', + requestType: + 'devvit.plugin.redditapi.newmodmail.GetConversationsRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.GetConversationsResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/CreateConversation', + name: 'CreateConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.CreateConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.CreateConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/GetConversation', + name: 'GetConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.GetConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.GetConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/CreateConversationMessage', + name: 'CreateConversationMessage', + requestType: + 'devvit.plugin.redditapi.newmodmail.CreateConversationMessageRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.CreateConversationMessageResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/ApproveConversation', + name: 'ApproveConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.ApproveConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/ArchiveConversation', + name: 'ArchiveConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.ArchiveConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/DisapproveConversation', + name: 'DisapproveConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.ApproveConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/UnhighlightConversation', + name: 'UnhighlightConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.HighlightConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/HighlightConversation', + name: 'HighlightConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.HighlightConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/MuteConversation', + name: 'MuteConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.MuteConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.MuteConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/TempBan', + name: 'TempBan', + requestType: 'devvit.plugin.redditapi.newmodmail.TempBanRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.TempBanResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/UnarchiveConversation', + name: 'UnarchiveConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.ArchiveConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.newmodmail.NewModmail/Unban', + name: 'Unban', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.TempBanResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/UnmuteConversation', + name: 'UnmuteConversation', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.MuteConversationResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/UserConversations', + name: 'UserConversations', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationRequest', + responseType: + 'devvit.plugin.redditapi.newmodmail.ConversationUserData', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.newmodmail.NewModmail/Read', + name: 'Read', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationsRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/Subreddits', + name: 'Subreddits', + requestType: 'google.protobuf.Empty', + responseType: + 'devvit.plugin.redditapi.newmodmail.SubredditsResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.newmodmail.NewModmail/Unread', + name: 'Unread', + requestType: + 'devvit.plugin.redditapi.newmodmail.BasicConversationsRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.newmodmail.NewModmail/UnreadCount', + name: 'UnreadCount', + requestType: 'google.protobuf.Empty', + responseType: + 'devvit.plugin.redditapi.newmodmail.UnreadCountResponse', + requestStream: false, + responseStream: false + } + ], + name: 'NewModmail', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'privatemessages.privatemessages.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.privatemessages.PrivateMessages', + methods: [ + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/Block', + name: 'Block', + requestType: + 'devvit.plugin.redditapi.privatemessages.GenericPrivateMessagesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/CollapseMessage', + name: 'CollapseMessage', + requestType: + 'devvit.plugin.redditapi.privatemessages.GenericPrivateMessagesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/Compose', + name: 'Compose', + requestType: + 'devvit.plugin.redditapi.privatemessages.ComposeRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/DelMsg', + name: 'DelMsg', + requestType: + 'devvit.plugin.redditapi.privatemessages.GenericPrivateMessagesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/ReadAllMessages', + name: 'ReadAllMessages', + requestType: + 'devvit.plugin.redditapi.privatemessages.ReadAllMessagesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/ReadMessage', + name: 'ReadMessage', + requestType: + 'devvit.plugin.redditapi.privatemessages.GenericPrivateMessagesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/UnblockSubreddit', + name: 'UnblockSubreddit', + requestType: + 'devvit.plugin.redditapi.privatemessages.GenericPrivateMessagesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/UncollapseMessage', + name: 'UncollapseMessage', + requestType: + 'devvit.plugin.redditapi.privatemessages.GenericPrivateMessagesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/UnreadMessage', + name: 'UnreadMessage', + requestType: + 'devvit.plugin.redditapi.privatemessages.GenericPrivateMessagesRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.privatemessages.PrivateMessages/MessageWhere', + name: 'MessageWhere', + requestType: + 'devvit.plugin.redditapi.privatemessages.MessageWhereRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + } + ], + name: 'PrivateMessages', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'subreddits.subreddits.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.subreddits.Subreddits', + methods: [ + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/AboutWhere', + name: 'AboutWhere', + requestType: + 'devvit.plugin.redditapi.subreddits.AboutWhereRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/DeleteSrBanner', + name: 'DeleteSrBanner', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/DeleteSrHeader', + name: 'DeleteSrHeader', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/DeleteSrIcon', + name: 'DeleteSrIcon', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/DeleteSrImg', + name: 'DeleteSrImg', + requestType: + 'devvit.plugin.redditapi.subreddits.DeleteSrImgRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SearchRedditNames', + name: 'SearchRedditNames', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSearchRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SearchRedditNamesResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SearchSubreddits', + name: 'SearchSubreddits', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSearchRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SearchSubredditsResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SiteAdmin', + name: 'SiteAdmin', + requestType: + 'devvit.plugin.redditapi.subreddits.SiteAdminRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubmitText', + name: 'SubmitText', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubmitTextResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditAutocomplete', + name: 'SubredditAutocomplete', + requestType: + 'devvit.plugin.redditapi.subreddits.SubredditAutocompleteRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditAutocompleteResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditStylesheet', + name: 'SubredditStylesheet', + requestType: + 'devvit.plugin.redditapi.subreddits.SubredditStylesheetRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/Subscribe', + name: 'Subscribe', + requestType: + 'devvit.plugin.redditapi.subreddits.SubscribeRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/UploadSrImg', + name: 'UploadSrImg', + requestType: + 'devvit.plugin.redditapi.subreddits.UploadSrImgRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.UploadSrImgResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditPostRequirements', + name: 'SubredditPostRequirements', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditPostRequirementsResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditAbout', + name: 'SubredditAbout', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditAboutResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditAboutEdit', + name: 'SubredditAboutEdit', + requestType: + 'devvit.plugin.redditapi.subreddits.SubredditAboutEditRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditAboutEditResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditAboutRules', + name: 'SubredditAboutRules', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditAboutRulesResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditAboutTraffic', + name: 'SubredditAboutTraffic', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditAboutTrafficResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/Sidebar', + name: 'Sidebar', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicSubredditRequest', + responseType: 'google.protobuf.StringValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.subreddits.Subreddits/Sticky', + name: 'Sticky', + requestType: 'devvit.plugin.redditapi.subreddits.StickyRequest', + responseType: 'devvit.plugin.redditapi.subreddits.StickyResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditsMineWhere', + name: 'SubredditsMineWhere', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicWhereRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditsSearch', + name: 'SubredditsSearch', + requestType: + 'devvit.plugin.redditapi.subreddits.SubredditsSearchRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditsSearchResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditsWhere', + name: 'SubredditsWhere', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicWhereRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditsSearchResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/UsersSearch', + name: 'UsersSearch', + requestType: + 'devvit.plugin.redditapi.subreddits.UsersSearchRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.UserSearchResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/UsersWhere', + name: 'UsersWhere', + requestType: + 'devvit.plugin.redditapi.subreddits.BasicWhereRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditsSearchResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditAddRemovalReason', + name: 'SubredditAddRemovalReason', + requestType: + 'devvit.plugin.redditapi.subreddits.SubredditAddRemovalReasonRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditAddRemovalReasonResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.subreddits.Subreddits/SubredditGetRemovalReasons', + name: 'SubredditGetRemovalReasons', + requestType: + 'devvit.plugin.redditapi.subreddits.SubredditGetRemovalReasonsRequest', + responseType: + 'devvit.plugin.redditapi.subreddits.SubredditGetRemovalReasonsResponse', + requestStream: false, + responseStream: false + } + ], + name: 'Subreddits', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'postcollections.postcollections.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.postcollections.PostCollections', + methods: [ + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/Collection', + name: 'Collection', + requestType: + 'devvit.plugin.redditapi.postcollections.CollectionRequest', + responseType: + 'devvit.plugin.redditapi.postcollections.CollectionResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/SubredditCollections', + name: 'SubredditCollections', + requestType: + 'devvit.plugin.redditapi.postcollections.SubredditCollectionsRequest', + responseType: + 'devvit.plugin.redditapi.postcollections.SubredditCollectionsResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/Create', + name: 'Create', + requestType: + 'devvit.plugin.redditapi.postcollections.CreateCollectionRequest', + responseType: + 'devvit.plugin.redditapi.postcollections.CollectionResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/Delete', + name: 'Delete', + requestType: + 'devvit.plugin.redditapi.postcollections.DeleteCollectionRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/AddPost', + name: 'AddPost', + requestType: + 'devvit.plugin.redditapi.postcollections.AddPostToCollectionRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/RemovePost', + name: 'RemovePost', + requestType: + 'devvit.plugin.redditapi.postcollections.RemovePostInCollectionRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/Follow', + name: 'Follow', + requestType: + 'devvit.plugin.redditapi.postcollections.FollowCollectionRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/Reorder', + name: 'Reorder', + requestType: + 'devvit.plugin.redditapi.postcollections.ReorderCollectionRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/UpdateTitle', + name: 'UpdateTitle', + requestType: + 'devvit.plugin.redditapi.postcollections.UpdateCollectionTitleRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/UpdateDescription', + name: 'UpdateDescription', + requestType: + 'devvit.plugin.redditapi.postcollections.UpdateCollectionDescriptionRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.postcollections.PostCollections/UpdateDisplayLayout', + name: 'UpdateDisplayLayout', + requestType: + 'devvit.plugin.redditapi.postcollections.UpdateCollectionDisplayLayoutRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + } + ], + name: 'PostCollections', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'users.users.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.users.Users', + methods: [ + { + fullName: '/devvit.plugin.redditapi.users.Users/BlockUser', + name: 'BlockUser', + requestType: 'devvit.plugin.redditapi.users.BlockUserRequest', + responseType: 'devvit.plugin.redditapi.users.BlockUserResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.users.Users/Friend', + name: 'Friend', + requestType: 'devvit.plugin.redditapi.users.FriendRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.users.Users/ReportUser', + name: 'ReportUser', + requestType: 'devvit.plugin.redditapi.users.ReportUserRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.users.Users/SetPermissions', + name: 'SetPermissions', + requestType: + 'devvit.plugin.redditapi.users.SetPermissionsRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.users.Users/Unfriend', + name: 'Unfriend', + requestType: 'devvit.plugin.redditapi.users.UnfriendRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.users.Users/UserDataByAccountIds', + name: 'UserDataByAccountIds', + requestType: + 'devvit.plugin.redditapi.users.UserDataByAccountIdsRequest', + responseType: + 'devvit.plugin.redditapi.users.UserDataByAccountIdsResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.users.Users/UsernameAvailable', + name: 'UsernameAvailable', + requestType: + 'devvit.plugin.redditapi.users.UsernameAvailableRequest', + responseType: 'google.protobuf.BoolValue', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.users.Users/UnfriendUser', + name: 'UnfriendUser', + requestType: 'devvit.plugin.redditapi.users.GenericUsersRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.users.Users/GetFriendInformation', + name: 'GetFriendInformation', + requestType: 'devvit.plugin.redditapi.users.GenericUsersRequest', + responseType: + 'devvit.plugin.redditapi.users.GeneralFriendResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.users.Users/UpdateFriendRelationship', + name: 'UpdateFriendRelationship', + requestType: + 'devvit.plugin.redditapi.users.UpdateFriendRelationshipRequest', + responseType: + 'devvit.plugin.redditapi.users.GeneralFriendResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.users.Users/UserTrophies', + name: 'UserTrophies', + requestType: 'devvit.plugin.redditapi.users.GenericUsersRequest', + responseType: + 'devvit.plugin.redditapi.users.UserTrophiesResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.users.Users/UserAbout', + name: 'UserAbout', + requestType: 'devvit.plugin.redditapi.users.UserAboutRequest', + responseType: 'devvit.plugin.redditapi.users.UserAboutResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.users.Users/UserWhere', + name: 'UserWhere', + requestType: 'devvit.plugin.redditapi.users.UserWhereRequest', + responseType: 'devvit.plugin.redditapi.common.Listing', + requestStream: false, + responseStream: false + } + ], + name: 'Users', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'widgets.widgets.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.widgets.Widgets', + methods: [ + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/AddButtonWidget', + name: 'AddButtonWidget', + requestType: + 'devvit.plugin.redditapi.widgets.AddButtonWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.ButtonWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/AddImageWidget', + name: 'AddImageWidget', + requestType: + 'devvit.plugin.redditapi.widgets.AddImageWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.ImageWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/AddCalendarWidget', + name: 'AddCalendarWidget', + requestType: + 'devvit.plugin.redditapi.widgets.AddCalendarWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.CalendarWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/AddTextAreaWidget', + name: 'AddTextAreaWidget', + requestType: + 'devvit.plugin.redditapi.widgets.AddTextAreaWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.TextAreaWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/AddCommunityListWidget', + name: 'AddCommunityListWidget', + requestType: + 'devvit.plugin.redditapi.widgets.AddCommunityListWidgetRequest', + responseType: + 'devvit.plugin.redditapi.widgets.CommunityListWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/AddPostFlairWidget', + name: 'AddPostFlairWidget', + requestType: + 'devvit.plugin.redditapi.widgets.AddPostFlairWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.PostFlairWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/AddCustomWidget', + name: 'AddCustomWidget', + requestType: + 'devvit.plugin.redditapi.widgets.AddCustomWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.CustomWidget', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.widgets.Widgets/DeleteWidget', + name: 'DeleteWidget', + requestType: + 'devvit.plugin.redditapi.widgets.DeleteWidgetRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/UpdateButtonWidget', + name: 'UpdateButtonWidget', + requestType: + 'devvit.plugin.redditapi.widgets.UpdateButtonWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.ButtonWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/UpdateImageWidget', + name: 'UpdateImageWidget', + requestType: + 'devvit.plugin.redditapi.widgets.UpdateImageWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.ImageWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/UpdateCalendarWidget', + name: 'UpdateCalendarWidget', + requestType: + 'devvit.plugin.redditapi.widgets.UpdateCalendarWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.CalendarWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/UpdateTextAreaWidget', + name: 'UpdateTextAreaWidget', + requestType: + 'devvit.plugin.redditapi.widgets.UpdateTextAreaWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.TextAreaWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/UpdateCommunityListWidget', + name: 'UpdateCommunityListWidget', + requestType: + 'devvit.plugin.redditapi.widgets.UpdateCommunityListWidgetRequest', + responseType: + 'devvit.plugin.redditapi.widgets.UpdateCommunityListWidgetResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/UpdatePostFlairWidget', + name: 'UpdatePostFlairWidget', + requestType: + 'devvit.plugin.redditapi.widgets.UpdatePostFlairWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.PostFlairWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/UpdateCustomWidget', + name: 'UpdateCustomWidget', + requestType: + 'devvit.plugin.redditapi.widgets.UpdateCustomWidgetRequest', + responseType: 'devvit.plugin.redditapi.widgets.CustomWidget', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.widgets.Widgets/GetWidgetImageUploadLease', + name: 'GetWidgetImageUploadLease', + requestType: + 'devvit.plugin.redditapi.widgets.GetWidgetImageUploadLeaseRequest', + responseType: + 'devvit.plugin.redditapi.widgets.GetWidgetImageUploadLeaseResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.widgets.Widgets/OrderWidgets', + name: 'OrderWidgets', + requestType: + 'devvit.plugin.redditapi.widgets.OrderWidgetsRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.widgets.Widgets/GetWidgets', + name: 'GetWidgets', + requestType: 'devvit.plugin.redditapi.widgets.GetWidgetsRequest', + responseType: + 'devvit.plugin.redditapi.widgets.GetWidgetsResponse', + requestStream: false, + responseStream: false + } + ], + name: 'Widgets', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'wiki.wiki.redditapi.system.local', + provides: [ + { + fullName: 'devvit.plugin.redditapi.wiki.Wiki', + methods: [ + { + fullName: '/devvit.plugin.redditapi.wiki.Wiki/GetWikiPages', + name: 'GetWikiPages', + requestType: 'devvit.plugin.redditapi.wiki.GetWikiPagesRequest', + responseType: 'devvit.plugin.redditapi.wiki.GetWikiPagesResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.wiki.Wiki/GetWikiPage', + name: 'GetWikiPage', + requestType: 'devvit.plugin.redditapi.wiki.GetWikiPageRequest', + responseType: 'devvit.plugin.redditapi.wiki.GetWikiPageResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.wiki.Wiki/EditWikiPage', + name: 'EditWikiPage', + requestType: 'devvit.plugin.redditapi.wiki.EditWikiPageRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.wiki.Wiki/HideWikiPageRevision', + name: 'HideWikiPageRevision', + requestType: + 'devvit.plugin.redditapi.wiki.HideWikiPageRevisionRequest', + responseType: + 'devvit.plugin.redditapi.wiki.HideWikiPageRevisionResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.wiki.Wiki/GetWikiPageRevisions', + name: 'GetWikiPageRevisions', + requestType: + 'devvit.plugin.redditapi.wiki.GetWikiPageRevisionsRequest', + responseType: + 'devvit.plugin.redditapi.wiki.WikiPageRevisionListing', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.wiki.Wiki/GetWikiRevisions', + name: 'GetWikiRevisions', + requestType: + 'devvit.plugin.redditapi.wiki.GetWikiRevisionsRequest', + responseType: + 'devvit.plugin.redditapi.wiki.WikiPageRevisionListing', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.wiki.Wiki/RevertWikiPage', + name: 'RevertWikiPage', + requestType: 'devvit.plugin.redditapi.wiki.RevertWikiPageRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.wiki.Wiki/GetWikiPageSettings', + name: 'GetWikiPageSettings', + requestType: + 'devvit.plugin.redditapi.wiki.GetWikiPageSettingsRequest', + responseType: 'devvit.plugin.redditapi.wiki.WikiPageSettings', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapi.wiki.Wiki/UpdateWikiPageSettings', + name: 'UpdateWikiPageSettings', + requestType: + 'devvit.plugin.redditapi.wiki.UpdateWikiPageSettingsRequest', + responseType: 'devvit.plugin.redditapi.wiki.WikiPageSettings', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.redditapi.wiki.Wiki/AllowEditor', + name: 'AllowEditor', + requestType: 'devvit.plugin.redditapi.wiki.AllowEditorRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + } + ], + name: 'Wiki', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'redditapiv2.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.redditapiv2.RedditAPIV2', + methods: [ + { + fullName: + '/devvit.plugin.redditapiv2.RedditAPIV2/GetSubredditCollections', + name: 'GetSubredditCollections', + requestType: + 'devvit.plugin.redditapi.postcollections.SubredditCollectionsRequest', + responseType: + 'devvit.plugin.redditapi.postcollections.SubredditCollectionsResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.redditapiv2.RedditAPIV2/DeleteFlairTemplate', + name: 'DeleteFlairTemplate', + requestType: + 'devvit.plugin.redditapi.flair.DeleteFlairTemplateRequest', + responseType: 'devvit.plugin.redditapi.common.JsonStatus', + requestStream: false, + responseStream: false + } + ], + name: 'RedditAPIV2', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'scheduler.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.scheduler.Scheduler', + methods: [ + { + fullName: '/devvit.plugin.scheduler.Scheduler/Schedule', + name: 'Schedule', + requestType: 'devvit.plugin.scheduler.ScheduledActionRequest', + responseType: 'devvit.plugin.scheduler.ScheduledActionResponse', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.scheduler.Scheduler/Cancel', + name: 'Cancel', + requestType: 'devvit.plugin.scheduler.CancelActionRequest', + responseType: 'google.protobuf.Empty', + requestStream: false, + responseStream: false + }, + { + fullName: '/devvit.plugin.scheduler.Scheduler/List', + name: 'List', + requestType: 'devvit.plugin.scheduler.ListActionRequest', + responseType: 'devvit.plugin.scheduler.ListActionResponse', + requestStream: false, + responseStream: false + } + ], + name: 'Scheduler', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'settings.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.settings.v1alpha.Settings', + methods: [ + { + fullName: '/devvit.plugin.settings.v1alpha.Settings/GetSettings', + name: 'GetSettings', + requestType: 'devvit.plugin.settings.v1alpha.SettingsRequest', + responseType: 'devvit.plugin.settings.v1alpha.SettingsResponse', + requestStream: false, + responseStream: false + } + ], + name: 'Settings', + version: '' + } + ], + code: '', + uses: [], + assets: {} + }, + { + actor: {name: 'default', owner: 'devvit', version: '1.0.0'}, + hostname: 'assetresolver.plugins.local', + provides: [ + { + fullName: 'devvit.plugin.assetresolver.AssetResolver', + methods: [ + { + fullName: + '/devvit.plugin.assetresolver.AssetResolver/GetAssetURL', + name: 'GetAssetURL', + requestType: 'devvit.plugin.assetresolver.GetAssetURLRequest', + responseType: 'devvit.plugin.assetresolver.GetAssetURLResponse', + requestStream: false, + responseStream: false + }, + { + fullName: + '/devvit.plugin.assetresolver.AssetResolver/GetAssetURLs', + name: 'GetAssetURLs', + requestType: + 'devvit.plugin.assetresolver.GetMultipleAssetURLsRequest', + responseType: + 'devvit.plugin.assetresolver.GetMultipleAssetURLsResponse', + requestStream: false, + responseStream: false + } + ], + name: 'AssetResolver', + version: '' + } + ], + code: '', + uses: [], + assets: {} + } + ] +} diff --git a/src/elements/play-editor/play-editor.test.ts b/src/elements/play-editor/play-editor.test.ts index ce25895..1e9d4ad 100644 --- a/src/elements/play-editor/play-editor.test.ts +++ b/src/elements/play-editor/play-editor.test.ts @@ -1,5 +1,5 @@ import {assert, expect} from '@esm-bundle/chai' -import {LitElement, html, render, type TemplateResult} from 'lit' +import {html, render, type TemplateResult} from 'lit' import {newTSEnv} from '../../bundler/compiler.js' import {PlayEditor} from './play-editor.js' @@ -11,31 +11,25 @@ test('tag is defined', () => { test('a slotted template emits an edit-template event', async () => { const edit = await new Promise>(async resolve => { document.body.addEventListener('edit-template', resolve) - using _el = await fixture( - 'play-editor', - html` - - - - ` - ) + using _el = await fixture(html` + + + + `) }) expect(edit.detail).equal("console.log('Hello World!')") }) test('setSrc() replaces a template', async () => { - using el = await fixture( - 'play-editor', - html` - - - - ` - ) + using el = await fixture(html` + + + + `) await new Promise(resolve => requestAnimationFrame(resolve)) const edit = await new Promise>(resolve => { el.addEventListener('edit', resolve) @@ -45,16 +39,14 @@ test('setSrc() replaces a template', async () => { expect(edit.detail).equal("'def'") }) -async function fixture( - tag: T, +async function fixture( template: TemplateResult -): Promise { +): Promise { const root = document.createElement('div') document.body.append(root) render(template, root) - const el = root.getElementsByTagName(tag)[0]! - if (el == null) throw Error(`no element tagged "${tag}"`) - if (el instanceof LitElement) await el.updateComplete - ;((el))[Symbol.dispose] = () => root.remove() - return el + const el = root.querySelector('play-editor')! + await el.updateComplete + el[Symbol.dispose] = () => root.remove() + return el } diff --git a/src/elements/play-pen-footer.ts b/src/elements/play-pen-footer.ts index 7e949bd..ca2fb34 100644 --- a/src/elements/play-pen-footer.ts +++ b/src/elements/play-pen-footer.ts @@ -6,17 +6,21 @@ import { type CSSResultGroup, type TemplateResult } from 'lit' -import {customElement, property, state} from 'lit/decorators.js' +import {customElement, property} from 'lit/decorators.js' import type {Diagnostics} from '../types/diagnostics.js' +import {Bubble} from '../utils/bubble.js' +import {cssReset} from '../utils/css-reset.js' import {openURL} from '../utils/open-url.js' -import {cssReset} from '../utils/css-reset.js' import './play-button.js' import './play-console.js' import './play-dropdown-menu.js' import './play-list-item.js' declare global { + interface HTMLElementEventMap { + 'open-console': CustomEvent + } interface HTMLElementTagNameMap { 'play-pen-footer': PlayPenFooter } @@ -62,7 +66,7 @@ export class PlayPenFooter extends LitElement { ` @property({attribute: false}) diagnostics?: Readonly - @state() private _open?: boolean + @property({attribute: 'open-console', type: Boolean}) openConsole?: boolean protected override render(): TemplateResult { const errsLen = @@ -74,9 +78,14 @@ export class PlayPenFooter extends LitElement { (this._open = !this._open)} + @click=${() => + this.dispatchEvent( + Bubble('open-console', !this.openConsole) + )} badge=${errsLen} label="Console" > @@ -108,8 +117,10 @@ export class PlayPenFooter extends LitElement { -
- ${this._open +
+ ${this.openConsole ? html` ` diff --git a/src/elements/play-pen-header.ts b/src/elements/play-pen-header.ts index ebff855..7b84bb8 100644 --- a/src/elements/play-pen-header.ts +++ b/src/elements/play-pen-header.ts @@ -6,17 +6,20 @@ import { type TemplateResult } from 'lit' import {customElement, property, query} from 'lit/decorators.js' +import {defaultSettings} from '../storage/settings-save.js' import {Bubble} from '../utils/bubble.js' +import {cssReset} from '../utils/css-reset.js' import {openURL} from '../utils/open-url.js' import type {PlayExportDialog} from './play-export-dialog.js' +import type {PlaySettingsDialog} from './play-settings-dialog.js' -import {cssReset} from '../utils/css-reset.js' import './play-button.js' import './play-export-dialog.js' import './play-icon/play-icon.js' import './play-logo/play-logo.js' import './play-new-pen-button.js' import './play-resizable-text-input.js' +import './play-settings-dialog.js' declare global { interface HTMLElementEventMap { @@ -67,10 +70,21 @@ export class PlayPenHeader extends LitElement { } ` + @property({attribute: 'allow-storage', type: Boolean}) allowStorage: boolean = + false @property() name: string = '' @property({attribute: false}) srcByLabel?: Readonly<{[key: string]: string}> + @property({attribute: 'remote-runtime-origin'}) remoteRuntimeOrigin: string = + defaultSettings.remoteRuntimeOrigin @property() url: string = '' + @property({attribute: 'use-experimental-blocks', type: Boolean}) + useExperimentalBlocks: boolean = false + @property({attribute: 'use-local-runtime', type: Boolean}) + useLocalRuntime: boolean = false + @property({attribute: 'use-remote-runtime', type: Boolean}) + useRemoteRuntime: boolean = false @query('play-export-dialog') private _export!: PlayExportDialog + @query('play-settings-dialog') private _settings!: PlaySettingsDialog protected override render(): TemplateResult { return html` @@ -105,6 +119,13 @@ export class PlayPenHeader extends LitElement { label="Export" @click=${() => this._export.open()} > this._settings.open()} + > + ` } } diff --git a/src/elements/play-pen/play-pen.ts b/src/elements/play-pen/play-pen.ts index 1a75bdb..831ad62 100644 --- a/src/elements/play-pen/play-pen.ts +++ b/src/elements/play-pen/play-pen.ts @@ -1,4 +1,4 @@ -import type {LinkedBundle} from '@devvit/protos' +import type {Empty, LinkedBundle} from '@devvit/protos' import {throttle} from '@devvit/shared-types/throttle.js' import type {DevvitUIError} from '@devvit/ui-renderer/client/devvit-custom-post.js' import type {VirtualTypeScriptEnvironment} from '@typescript/vfs' @@ -8,6 +8,7 @@ import { html, unsafeCSS, type CSSResultGroup, + type PropertyValues, type TemplateResult } from 'lit' import {customElement, property, query, state} from 'lit/decorators.js' @@ -25,9 +26,16 @@ import helloBlocks from '../../examples/hello-blocks.example.tsx' import polls from '../../examples/polls.example.tsx' import progressBar from '../../examples/progress-bar.example.tsx' import svg from '../../examples/svg.example.tsx' +import {BundleStore} from '../../runtime/bundle-store.js' +import {PenSave, loadPen, penToHash, savePen} from '../../storage/pen-save.js' +import { + defaultSettings, + loadSettings, + saveSettings +} from '../../storage/settings-save.js' import type {ColorScheme} from '../../types/color-scheme.js' import type {Diagnostics} from '../../types/diagnostics.js' -import {PenSave, loadPen, penToHash, savePen} from '../../types/pen-save.js' +import {newHostname} from '../../utils/compute-util.js' import {cssReset} from '../../utils/css-reset.js' import type {OpenLine} from '../play-console.js' import type {PlayEditor} from '../play-editor/play-editor.js' @@ -132,11 +140,21 @@ export class PlayPen extends LitElement { /** Program executable. */ @state() private _bundle?: Readonly | undefined /** Execution preview widths. */ - @state() private _previewWidth: number = 288 @state() private _diagnostics: Diagnostics = {previewErrs: [], tsErrs: []} + @state() private _openConsole: boolean = false + @state() private _previewWidth: number = 288 + @state() private _remoteRuntimeOrigin: string = + defaultSettings.remoteRuntimeOrigin + @state() private _useExperimentalBlocks: boolean = false + @state() private _useLocalRuntime: boolean = false + @state() private _useRemoteRuntime: boolean = false @query('play-editor') private _editor!: PlayEditor @query('play-toast') private _toast!: PlayToast + #bundleStore?: BundleStore | undefined readonly #env: VirtualTypeScriptEnvironment = newTSEnv() + @state() _uploaded: Promise = Promise.resolve({}) + /** Try to ensure the bundle hostname is unique. See compute-util. */ + #version: number = Date.now() /** Program title. */ @state() private _name: string = '' /** Execution color scheme. */ @state() private _scheme: @@ -149,6 +167,19 @@ export class PlayPen extends LitElement { override connectedCallback(): void { super.connectedCallback() + const settings = + (this.allowStorage ? loadSettings(localStorage) : undefined) ?? + defaultSettings + if (settings) { + this._openConsole = settings.openConsole + this._remoteRuntimeOrigin = settings.remoteRuntimeOrigin + this._useExperimentalBlocks = settings.useExperimentalBlocks + this._useLocalRuntime = settings.useLocalRuntime + this._useRemoteRuntime = settings.useRemoteRuntime + // If remote is enabled, #bundleStore is initialized in willUpdate() and + // bundle is loaded. + } + let pen if (this.allowURL) pen = loadPen(location) if (this.allowStorage) pen ??= loadPen(localStorage) @@ -165,9 +196,14 @@ export class PlayPen extends LitElement { return html` Copied the URL!) => this.#setName(ev.detail, true)} @edit-src=${(ev: CustomEvent) => { @@ -175,6 +211,14 @@ export class PlayPen extends LitElement { this.#setName('', false) this._editor.setSrc(ev.detail) }} + @use-experimental-blocks=${(ev: CustomEvent) => + (this._useExperimentalBlocks = ev.detail)} + @use-local-runtime=${(ev: CustomEvent) => + (this._useLocalRuntime = ev.detail)} + @use-remote-runtime=${(ev: CustomEvent) => + (this._useRemoteRuntime = ev.detail)} + @edit-remote-runtime-origin=${(ev: CustomEvent) => + (this._remoteRuntimeOrigin = ev.detail)} @share=${this.#onShare} >
@@ -195,7 +239,12 @@ export class PlayPen extends LitElement { this.#clearPreviewErrors()} @devvit-ui-error=${(ev: CustomEvent) => this.#appendPreviewError(ev.detail)} @@ -215,6 +264,9 @@ export class PlayPen extends LitElement {
) => + (this._openConsole = ev.detail)} @preview-width=${(ev: CustomEvent) => (this._previewWidth = ev.detail)} @preview-scheme=${(ev: CustomEvent) => @@ -225,6 +277,42 @@ export class PlayPen extends LitElement { ` } + protected override async willUpdate( + props: PropertyValues & + PropertyValues<{ + _openConsole: boolean + _useExperimentalBlocks: boolean + _useLocalRuntime: boolean + _useRemoteRuntime: boolean + _remoteRuntimeOrigin: string + }> + ): Promise { + super.willUpdate(props) + + if ( + this.allowStorage && + (props.has('_openConsole') || + props.has('_useExperimentalBlocks') || + props.has('_remoteRuntimeOrigin') || + props.has('_useLocalRuntime') || + props.has('_useRemoteRuntime')) + ) + saveSettings(localStorage, { + openConsole: this._openConsole, + useExperimentalBlocks: this._useExperimentalBlocks, + useLocalRuntime: this._useLocalRuntime, + useRemoteRuntime: this._useRemoteRuntime, + remoteRuntimeOrigin: this._remoteRuntimeOrigin, + version: 1 + }) + + if (props.has('_remoteRuntimeOrigin')) + this.#bundleStore = BundleStore(this._remoteRuntimeOrigin) + + if (props.has('_useRemoteRuntime') || props.has('_remoteRuntimeOrigin')) + this.#upload() + } + #appendPreviewError(err: DevvitUIError): void { this._diagnostics = { ...this._diagnostics, @@ -270,9 +358,14 @@ export class PlayPen extends LitElement { } /** Throttled changes after updating sources. */ - #setSrcSideEffects = throttle((save: boolean) => { - this._bundle = link(compile(this.#env)) + #setSrcSideEffects = throttle((save: boolean): void => { + this.#version++ + this._bundle = link( + compile(this.#env), + newHostname(this._name, this.#version) + ) if (save) this.#save() + this.#upload() }, 500) /** Recompute the current hash regardless of the location bar state. */ @@ -281,4 +374,12 @@ export class PlayPen extends LitElement { url.hash = penToHash(PenSave(this._name, this._src ?? '')) return url } + + #upload(): void { + // Unlike production, bundles are first available locally and then remotely. + // Upload failures appear in the execution of RemoteApp. + if (this._useRemoteRuntime && this._bundle) + this._uploaded = + this.#bundleStore?.upload(this._bundle) ?? Promise.resolve({}) + } } diff --git a/src/elements/play-preview.test.ts b/src/elements/play-preview.test.ts deleted file mode 100644 index 86a02ed..0000000 --- a/src/elements/play-preview.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {assert} from '@esm-bundle/chai' -import {PlayPreview} from './play-preview.js' - -test('tag is defined', () => { - const el = document.createElement('play-preview') - assert.instanceOf(el, PlayPreview) -}) diff --git a/src/elements/play-preview.ts b/src/elements/play-preview.ts index b6eb4de..7019882 100644 --- a/src/elements/play-preview.ts +++ b/src/elements/play-preview.ts @@ -1,7 +1,7 @@ // @ts-expect-error import * as penWorker from '@devvit/previews/dist/pen.worker.min.js' -import type {LinkedBundle, Metadata} from '@devvit/protos' +import type {Empty, LinkedBundle, Metadata} from '@devvit/protos' import { LitElement, css, @@ -12,11 +12,13 @@ import { type TemplateResult } from 'lit' import {customElement, property, state} from 'lit/decorators.js' +import {RemoteApp} from '../runtime/remote-app.js' +import {defaultSettings} from '../storage/settings-save.js' import type {ColorScheme} from '../types/color-scheme.js' import {Bubble} from '../utils/bubble.js' +import {cssReset} from '../utils/css-reset.js' import '@devvit/previews/dist/devvit-preview.js' -import {cssReset} from '../utils/css-reset.js' const localRuntimeCode: Blob = new Blob([penWorker.default], { type: 'text/javascript' @@ -83,15 +85,31 @@ export class PlayPreview extends LitElement { @property({attribute: false}) bundle: Readonly | undefined @property({type: Number}) previewWidth?: number + @property({attribute: 'remote-runtime-origin'}) remoteRuntimeOrigin: string = + defaultSettings.remoteRuntimeOrigin @property() scheme?: ColorScheme + @property({attribute: false}) uploaded: Promise | undefined + @property({attribute: 'use-experimental-blocks', type: Boolean}) + useExperimentalBlocks: boolean = false + @property({attribute: 'use-local-runtime', type: Boolean}) + useLocalRuntime: boolean = false + @property({attribute: 'use-remote-runtime', type: Boolean}) + useRemoteRuntime: boolean = false @state() private _err = false #meta: Metadata = { - 'devvit-app-user': {values: ['t2_appuser']}, - 'devvit-subreddit': {values: ['t5_sub']}, - 'devvit-user': {values: ['t2_user']} + 'actor-id': {values: []}, // Set in willUpdate(). + 'devvit-actor': {values: ['main']}, + 'devvit-app': {values: ['pen']}, + 'devvit-app-user': {values: ['t2_123']}, + 'devvit-r2-host': {values: ['oauth.reddit.com']}, + 'devvit-installation': {values: ['123']}, + 'devvit-subreddit': {values: ['t5_123']}, + 'devvit-user': {values: ['t2_123']}, + 'devvit-user-agent': {values: ['play']} } + #remote: RemoteApp | undefined protected override render(): TemplateResult { // to-do: don't override toaster's --rem16 to offset the toast. Upstream a @@ -102,11 +120,15 @@ export class PlayPreview extends LitElement { ? html` (this._err = true)} > @@ -122,7 +144,13 @@ export class PlayPreview extends LitElement { super.willUpdate(props) if (props.has('bundle')) { this._err = false + if (this.bundle) this.#meta['actor-id']!.values = [this.bundle.hostname] this.dispatchEvent(Bubble('clear-errors', undefined)) } + if (props.has('remoteRuntimeOrigin')) + this.#remote = RemoteApp.new( + this.remoteRuntimeOrigin, + () => this.uploaded ?? Promise.resolve({}) + ) } } diff --git a/src/elements/play-settings-dialog.test.ts b/src/elements/play-settings-dialog.test.ts new file mode 100644 index 0000000..3b6abed --- /dev/null +++ b/src/elements/play-settings-dialog.test.ts @@ -0,0 +1,7 @@ +import {assert} from '@esm-bundle/chai' +import {PlaySettingsDialog} from './play-settings-dialog.js' + +test('tag is defined', () => { + const el = document.createElement('play-settings-dialog') + assert.instanceOf(el, PlaySettingsDialog) +}) diff --git a/src/elements/play-settings-dialog.ts b/src/elements/play-settings-dialog.ts new file mode 100644 index 0000000..bbc99ab --- /dev/null +++ b/src/elements/play-settings-dialog.ts @@ -0,0 +1,215 @@ +import { + LitElement, + css, + html, + type CSSResultGroup, + type TemplateResult +} from 'lit' +import {customElement, property, query} from 'lit/decorators.js' +import {defaultSettings} from '../storage/settings-save.js' +import {Bubble} from '../utils/bubble.js' +import {cssReset} from '../utils/css-reset.js' + +import './play-button.js' + +declare global { + interface HTMLElementEventMap { + 'use-experimental-blocks': CustomEvent + 'use-local-runtime': CustomEvent + 'use-remote-runtime': CustomEvent + 'edit-remote-runtime-origin': CustomEvent + } + interface HTMLElementTagNameMap { + 'play-settings-dialog': PlaySettingsDialog + } +} + +@customElement('play-settings-dialog') +export class PlaySettingsDialog extends LitElement { + static override readonly styles: CSSResultGroup = css` + ${cssReset} + + dialog { + color: var(--color-neutral-content); + background-color: var(--color-neutral-background); + box-shadow: var(--shadow-m); + + border-bottom-left-radius: var(--radius); + border-bottom-right-radius: var(--radius); + border-top-left-radius: var(--radius); + border-top-right-radius: var(--radius); + + padding-top: var(--space); + padding-bottom: var(--space); + padding-left: var(--space); + padding-right: var(--space); + + /* No border needed. Dialog background has sufficient contrast against the scrim. */ + border-width: 0; + + /* to-do: breakpoints */ + width: 480px; + max-width: 90vw; + } + + dialog::backdrop { + /* to-do: Update to css variable --color-shade-60 once supported by Chromium and Safari. + https://bugs.webkit.org/show_bug.cgi?id=263834 + https://bugs.chromium.org/p/chromium/issues/detail?id=827397 + https://stackoverflow.com/a/77393321 */ + background-color: rgba(0, 0, 0, 0.6); + } + + header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + row-gap: var(--space); + } + + h1 { + color: var(--color-neutral-content-strong); + + /* No margins needed for composition. */ + margin-top: 0; + margin-bottom: 0; + + /* RPL/Heading Bold/24-HeadingBold */ + font-family: var(--font-family-sans); + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 28px; + letter-spacing: 0.2px; + } + + input[type='checkbox'] { + float: left; + margin-inline-end: var(--space); + } + + input[type='text'] { + display: block; + width: 100%; + } + + label { + cursor: pointer; /* Finger. */ + display: block; /* Each control starts a new block. */ + margin-bottom: var(--space); + } + + legend { + font-weight: bold; + } + ` + + @property({attribute: 'allow-storage', type: Boolean}) allowStorage: boolean = + false + @property({attribute: 'remote-runtime-origin'}) remoteRuntimeOrigin: string = + defaultSettings.remoteRuntimeOrigin + @property({attribute: 'use-experimental-blocks', type: Boolean}) + useExperimentalBlocks: boolean = false + @property({attribute: 'use-local-runtime', type: Boolean}) + useLocalRuntime: boolean = false + @property({attribute: 'use-remote-runtime', type: Boolean}) + useRemoteRuntime: boolean = false + @query('dialog') private _dialog!: HTMLDialogElement + + open(): void { + this._dialog.showModal() + } + + close(): void { + this._dialog.close() + } + + protected override render(): TemplateResult { + return html` + +
+

Settings

+ +
+ +

+ Settings are ${this.allowStorage ? 'saved and ' : ''}not shareable. +

+ +
+ Reddit Internal +

Runtime settings take effect on subsequent execution.

+ + + + +
+
+ ` + } +} + +/** Translate a boolean to on / off. */ +function onOff(on: boolean): string { + return on ? 'on' : 'off' +} diff --git a/src/index.ts b/src/index.ts index a57e4b3..ea9d925 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export const playVersion = globalThis.playVersion export const playDevvitVersion = globalThis.devvitVersion -export * from './types/pen-save.js' +export * from './storage/pen-save.js' diff --git a/src/runtime/bundle-store.ts b/src/runtime/bundle-store.ts new file mode 100644 index 0000000..2ff7340 --- /dev/null +++ b/src/runtime/bundle-store.ts @@ -0,0 +1,15 @@ +import { + BundleServiceDefinition, + type Empty, + type LinkedBundle +} from '@devvit/protos' +import {createChannel, createClient} from 'nice-grpc-web' + +export type BundleStore = { + upload(bundle: Readonly): Promise +} + +export function BundleStore(gatewayOrigin: string): BundleStore { + const channel = createChannel(gatewayOrigin) + return (createClient(BundleServiceDefinition, channel)) +} diff --git a/src/runtime/remote-app.ts b/src/runtime/remote-app.ts new file mode 100644 index 0000000..b467aa6 --- /dev/null +++ b/src/runtime/remote-app.ts @@ -0,0 +1,124 @@ +import { + CustomPostDefinition, + UIEventHandlerDefinition, + type Empty, + type HandleUIEventRequest, + type HandleUIEventResponse, + type Metadata, + type RenderPostRequest, + type RenderPostResponse, + type UIRequest, + type UIResponse +} from '@devvit/protos' +import type {UIApp} from '@devvit/ui-renderer/client/ui-app.js' +import { + Metadata as NiceMeta, + createChannel, + createClient, + type CallOptions +} from 'nice-grpc-web' + +// to-do: fix upstream ts-proto workarounds and ts-proto / nice-grpc-web +// integration typing (DX-5961). The definition types in @devvit/protos like +// CustomPostDefinition are simplified to workaround: +// +// The inferred type of this node exceeds the maximum length the compiler will +// serialize. An explicit type annotation is needed.ts(7056) +// +// Redefining the definitions in play gives the same error. However, even +// compilable definitions like UIEventHandlerDefinition generate incorrect +// client typing with nice-grpc-web. +type CustomPostNiceClient = { + renderPost( + req: Readonly, + opts: Readonly + ): Promise + renderPostComposer( + req: Readonly, + opts: Readonly + ): Promise + renderPostContent( + req: Readonly, + opts: Readonly + ): Promise +} +type UIEventHandlerNiceClient = { + handleUIEvent( + req: Readonly, + opts: Readonly + ): Promise +} + +export class RemoteApp implements UIApp { + /** @arg uploaded Callback to test whether remote bundle is available. */ + static new(gatewayOrigin: string, uploaded: () => Promise): RemoteApp { + const channel = createChannel(gatewayOrigin) + const customPost = createClient(CustomPostDefinition, channel) + const uiEventHandler = createClient(UIEventHandlerDefinition, channel) + return new RemoteApp( + (customPost), + (uiEventHandler), + uploaded + ) + } + + #customPost: CustomPostNiceClient + #uiEventHandler: UIEventHandlerNiceClient + #uploaded: () => Promise + + constructor( + customPost: CustomPostNiceClient, + uiEventHandler: UIEventHandlerNiceClient, + uploaded: () => Promise + ) { + this.#customPost = customPost + this.#uiEventHandler = uiEventHandler + this.#uploaded = uploaded + } + + async HandleUIEvent( + req: Readonly, + meta: Readonly | undefined + ): Promise { + await this.#uploaded() + return this.#uiEventHandler.handleUIEvent(req, { + metadata: newNiceMeta(meta) + }) + } + + async RenderPost( + req: Readonly, + meta: Readonly | undefined + ): Promise { + await this.#uploaded() + return this.#customPost.renderPost(req, { + metadata: newNiceMeta(meta) + }) + } + + async RenderPostContent( + req: Readonly, + meta: Readonly | undefined + ): Promise { + await this.#uploaded() + return this.#customPost.renderPostContent(req, { + metadata: newNiceMeta(meta) + }) + } + + async RenderPostComposer( + req: Readonly, + meta: Readonly | undefined + ): Promise { + await this.#uploaded() + return this.#customPost.renderPostComposer(req, { + metadata: newNiceMeta(meta) + }) + } +} + +function newNiceMeta(meta: Readonly | undefined): NiceMeta { + return new NiceMeta( + Object.entries(meta ?? {}).map(([key, {values}]) => [key, values.join()]) + ) +} diff --git a/src/types/pen-save.test.ts b/src/storage/pen-save.test.ts similarity index 100% rename from src/types/pen-save.test.ts rename to src/storage/pen-save.test.ts diff --git a/src/types/pen-save.ts b/src/storage/pen-save.ts similarity index 100% rename from src/types/pen-save.ts rename to src/storage/pen-save.ts diff --git a/src/storage/settings-save.ts b/src/storage/settings-save.ts new file mode 100644 index 0000000..7512686 --- /dev/null +++ b/src/storage/settings-save.ts @@ -0,0 +1,52 @@ +/** Settings state for un/packing to/from LocalStorage. Not shareable. */ +export type SettingsSave = { + /** Most recent console open state. */ + openConsole: boolean + useExperimentalBlocks: boolean + /** Enable local runtime. Execute apps locally whenever possible. */ + useLocalRuntime: boolean + /** Enable remote runtime. Upload often and execute apps remotely as needed. */ + useRemoteRuntime: boolean + /** Probably the devenv compute address. Eg, http://localhost:7777. */ + remoteRuntimeOrigin: string + /** + * Settings version recorded at save time. Used for unpacking old data if + * structural changes have been made. Independent of package.json version. + */ + version: 1 +} + +const storageKey = 'playSettings' + +export const defaultSettings: Readonly = { + openConsole: false, + useExperimentalBlocks: false, + useLocalRuntime: true, + useRemoteRuntime: false, + remoteRuntimeOrigin: 'http://localhost:7777', + version: 1 +} + +export function loadSettings( + storage: Readonly +): SettingsSave | undefined { + return settingsFromJSON(storage.getItem(storageKey) ?? '') +} + +export function saveSettings( + storage: Readonly, + settings: Readonly +): void { + storage.setItem(storageKey, JSON.stringify(settings)) +} + +function settingsFromJSON(json: string): SettingsSave | undefined { + let settings + try { + settings = JSON.parse(json) + } catch { + return + } + if (!settings || settings.version !== 1) return + return settings // Assume LocalStorage is valid. +} diff --git a/src/utils/compute-util.ts b/src/utils/compute-util.ts new file mode 100644 index 0000000..58b3c73 --- /dev/null +++ b/src/utils/compute-util.ts @@ -0,0 +1,5 @@ +// to-do: compute-go doesn't seem to replace old bundles. Generate a unique +// hostname for each new build. +export function newHostname(name: string, version: number): string { + return `${name.toLocaleLowerCase().replace(/[^a-z0-9]/, '') || 'untitled'}-${version}.local` +}