creek 0.2.2 → 0.3.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/commands/deploy.d.ts +8 -0
  2. package/dist/commands/deploy.js +146 -0
  3. package/dist/commands/init.d.ts +8 -0
  4. package/dist/commands/init.js +60 -0
  5. package/dist/commands/login.d.ts +8 -0
  6. package/dist/commands/login.js +32 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +19 -0
  9. package/dist/utils/bundle.d.ts +6 -0
  10. package/dist/utils/bundle.js +24 -0
  11. package/dist/utils/config.d.ts +11 -0
  12. package/dist/utils/config.js +33 -0
  13. package/dist/utils/ssr-bundle.d.ts +6 -0
  14. package/dist/utils/ssr-bundle.js +48 -0
  15. package/package.json +38 -16
  16. package/README.md +0 -105
  17. package/bin/creek +0 -10
  18. package/examples/hello.creek +0 -6
  19. package/examples/twitter.creek +0 -23
  20. package/examples/zeromq.creek +0 -16
  21. package/lib/aggregator.coffee +0 -27
  22. package/lib/aggregators/count.coffee +0 -15
  23. package/lib/aggregators/distinct.coffee +0 -20
  24. package/lib/aggregators/max.coffee +0 -16
  25. package/lib/aggregators/mean.coffee +0 -27
  26. package/lib/aggregators/min.coffee +0 -16
  27. package/lib/aggregators/popular.coffee +0 -27
  28. package/lib/aggregators/recent.coffee +0 -10
  29. package/lib/aggregators/sum.coffee +0 -17
  30. package/lib/bootstrap.js +0 -5
  31. package/lib/compound-aggregator.coffee +0 -33
  32. package/lib/creek.coffee +0 -13
  33. package/lib/interfaces/rest.coffee +0 -30
  34. package/lib/interfaces/websocket.coffee +0 -15
  35. package/lib/lazy-bucketed-aggregator.coffee +0 -35
  36. package/lib/parsers/chunked.coffee +0 -17
  37. package/lib/parsers/json.coffee +0 -9
  38. package/lib/parsers/words.coffee +0 -6
  39. package/lib/parsers/zeromq.coffee +0 -16
  40. package/lib/runner.coffee +0 -39
  41. package/lib/timeboxed-aggregator.coffee +0 -64
@@ -0,0 +1,8 @@
1
+ export declare const deployCommand: import("citty").CommandDef<{
2
+ "skip-build": {
3
+ type: "boolean";
4
+ description: string;
5
+ default: false;
6
+ };
7
+ }>;
8
+ //# sourceMappingURL=deploy.d.ts.map
@@ -0,0 +1,146 @@
1
+ import { defineCommand } from "citty";
2
+ import consola from "consola";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { execSync } from "node:child_process";
6
+ import { parseConfig, CreekClient, isSSRFramework, getSSRServerEntry, getClientAssetsDir, } from "@solcreek/sdk";
7
+ import { getToken, getApiUrl } from "../utils/config.js";
8
+ import { collectAssets } from "../utils/bundle.js";
9
+ import { bundleSSRServer } from "../utils/ssr-bundle.js";
10
+ export const deployCommand = defineCommand({
11
+ meta: {
12
+ name: "deploy",
13
+ description: "Deploy the current project to Creek",
14
+ },
15
+ args: {
16
+ "skip-build": {
17
+ type: "boolean",
18
+ description: "Skip the build step",
19
+ default: false,
20
+ },
21
+ },
22
+ async run({ args }) {
23
+ const cwd = process.cwd();
24
+ const configPath = join(cwd, "creek.toml");
25
+ if (!existsSync(configPath)) {
26
+ consola.error("No creek.toml found. Run `creek init` first.");
27
+ process.exit(1);
28
+ }
29
+ const config = parseConfig(readFileSync(configPath, "utf-8"));
30
+ consola.info(`Project: ${config.project.name}`);
31
+ const token = getToken();
32
+ if (!token) {
33
+ consola.error("Not authenticated. Run `creek login` first.");
34
+ process.exit(1);
35
+ }
36
+ const client = new CreekClient(getApiUrl(), token);
37
+ // Ensure project exists
38
+ let project;
39
+ try {
40
+ project = await client.getProject(config.project.name);
41
+ }
42
+ catch {
43
+ consola.info("Project not found, creating...");
44
+ const res = await client.createProject({
45
+ slug: config.project.name,
46
+ framework: config.project.framework,
47
+ });
48
+ project = res.project;
49
+ consola.success(`Created project: ${project.slug}`);
50
+ }
51
+ // Build
52
+ if (!args["skip-build"]) {
53
+ consola.start(`Building with: ${config.build.command}`);
54
+ try {
55
+ execSync(config.build.command, { cwd, stdio: "inherit" });
56
+ }
57
+ catch {
58
+ consola.error("Build failed");
59
+ process.exit(1);
60
+ }
61
+ consola.success("Build complete");
62
+ }
63
+ const outputDir = resolve(cwd, config.build.output);
64
+ if (!existsSync(outputDir)) {
65
+ consola.error(`Build output directory not found: ${config.build.output}`);
66
+ process.exit(1);
67
+ }
68
+ const framework = config.project.framework ?? null;
69
+ const isSSR = isSSRFramework(framework);
70
+ const renderMode = isSSR ? "ssr" : "spa";
71
+ // Collect client assets
72
+ let clientAssetsDir = outputDir;
73
+ if (isSSR && framework) {
74
+ const clientSubdir = getClientAssetsDir(framework);
75
+ if (clientSubdir) {
76
+ clientAssetsDir = resolve(outputDir, clientSubdir);
77
+ }
78
+ }
79
+ consola.start("Collecting assets...");
80
+ const { assets: clientAssets, fileList } = collectAssets(clientAssetsDir);
81
+ consola.info(`Found ${fileList.length} client assets`);
82
+ // Bundle server files for SSR
83
+ let serverFiles;
84
+ if (isSSR && framework) {
85
+ const serverEntry = getSSRServerEntry(framework);
86
+ if (serverEntry) {
87
+ const serverEntryPath = resolve(outputDir, serverEntry);
88
+ if (existsSync(serverEntryPath)) {
89
+ consola.start("Bundling SSR server...");
90
+ const bundled = await bundleSSRServer(serverEntryPath);
91
+ // Send as base64-encoded single file
92
+ serverFiles = {
93
+ "server.js": Buffer.from(bundled).toString("base64"),
94
+ };
95
+ consola.success(`SSR server bundled (${Math.round(bundled.length / 1024)}KB)`);
96
+ }
97
+ }
98
+ }
99
+ // Create deployment
100
+ consola.start("Creating deployment...");
101
+ const { deployment } = await client.createDeployment(project.id);
102
+ // Upload bundle
103
+ consola.start("Uploading...");
104
+ const bundle = {
105
+ manifest: {
106
+ assets: fileList,
107
+ hasWorker: isSSR,
108
+ entrypoint: null,
109
+ renderMode,
110
+ },
111
+ workerScript: null,
112
+ assets: clientAssets,
113
+ serverFiles,
114
+ };
115
+ const result = await client.uploadDeploymentBundle(project.id, deployment.id, bundle);
116
+ if (result.url) {
117
+ consola.success(`Deployed! ${result.url}`);
118
+ if (result.previewUrl) {
119
+ consola.info(`Preview: ${result.previewUrl}`);
120
+ }
121
+ }
122
+ else {
123
+ let status = deployment.status;
124
+ let url = null;
125
+ for (let i = 0; i < 30; i++) {
126
+ const res = await client.getDeploymentStatus(project.id, deployment.id);
127
+ status = res.deployment.status;
128
+ url = res.url;
129
+ if (status === "active" || status === "failed")
130
+ break;
131
+ await new Promise((r) => setTimeout(r, 1000));
132
+ }
133
+ if (status === "active" && url) {
134
+ consola.success(`Deployed! ${url}`);
135
+ }
136
+ else if (status === "failed") {
137
+ consola.error("Deployment failed");
138
+ process.exit(1);
139
+ }
140
+ else {
141
+ consola.warn(`Deployment status: ${status}`);
142
+ }
143
+ }
144
+ },
145
+ });
146
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1,8 @@
1
+ export declare const initCommand: import("citty").CommandDef<{
2
+ name: {
3
+ type: "string";
4
+ description: string;
5
+ required: false;
6
+ };
7
+ }>;
8
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { defineCommand } from "citty";
2
+ import consola from "consola";
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join, basename } from "node:path";
5
+ import { stringify } from "smol-toml";
6
+ import { detectFramework } from "@solcreek/sdk";
7
+ export const initCommand = defineCommand({
8
+ meta: {
9
+ name: "init",
10
+ description: "Initialize a new Creek project",
11
+ },
12
+ args: {
13
+ name: {
14
+ type: "string",
15
+ description: "Project name",
16
+ required: false,
17
+ },
18
+ },
19
+ async run({ args }) {
20
+ const cwd = process.cwd();
21
+ const configPath = join(cwd, "creek.toml");
22
+ if (existsSync(configPath)) {
23
+ consola.warn("creek.toml already exists");
24
+ const overwrite = await consola.prompt("Overwrite?", { type: "confirm" });
25
+ if (!overwrite)
26
+ return;
27
+ }
28
+ // Detect framework
29
+ const pkgPath = join(cwd, "package.json");
30
+ let framework;
31
+ if (existsSync(pkgPath)) {
32
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
33
+ const detected = detectFramework(pkg);
34
+ if (detected) {
35
+ framework = detected;
36
+ consola.info(`Detected framework: ${framework}`);
37
+ }
38
+ }
39
+ const defaultName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
40
+ const name = args.name ?? defaultName;
41
+ const config = {
42
+ project: {
43
+ name,
44
+ ...(framework ? { framework } : {}),
45
+ },
46
+ build: {
47
+ command: "npm run build",
48
+ output: "dist",
49
+ },
50
+ resources: {
51
+ d1: false,
52
+ kv: false,
53
+ r2: false,
54
+ },
55
+ };
56
+ writeFileSync(configPath, stringify(config));
57
+ consola.success(`Created creek.toml for "${name}"`);
58
+ },
59
+ });
60
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,8 @@
1
+ export declare const loginCommand: import("citty").CommandDef<{
2
+ token: {
3
+ type: "string";
4
+ description: string;
5
+ required: false;
6
+ };
7
+ }>;
8
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { defineCommand } from "citty";
2
+ import consola from "consola";
3
+ import { writeCliConfig, readCliConfig } from "../utils/config.js";
4
+ export const loginCommand = defineCommand({
5
+ meta: {
6
+ name: "login",
7
+ description: "Authenticate with Creek using an API token",
8
+ },
9
+ args: {
10
+ token: {
11
+ type: "string",
12
+ description: "API token",
13
+ required: false,
14
+ },
15
+ },
16
+ async run({ args }) {
17
+ let token = args.token;
18
+ if (!token) {
19
+ token = await consola.prompt("Enter your API token:", {
20
+ type: "text",
21
+ });
22
+ }
23
+ if (!token || typeof token !== "string") {
24
+ consola.error("No token provided");
25
+ process.exit(1);
26
+ }
27
+ const config = readCliConfig();
28
+ writeCliConfig({ ...config, token });
29
+ consola.success("Token saved successfully");
30
+ },
31
+ });
32
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { defineCommand, runMain } from "citty";
3
+ import { loginCommand } from "./commands/login.js";
4
+ import { initCommand } from "./commands/init.js";
5
+ import { deployCommand } from "./commands/deploy.js";
6
+ const main = defineCommand({
7
+ meta: {
8
+ name: "creek",
9
+ version: "0.1.0",
10
+ description: "Deploy full-stack apps to the edge",
11
+ },
12
+ subCommands: {
13
+ login: loginCommand,
14
+ init: initCommand,
15
+ deploy: deployCommand,
16
+ },
17
+ });
18
+ runMain(main);
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ export interface BundleAssets {
2
+ assets: Record<string, string>;
3
+ fileList: string[];
4
+ }
5
+ export declare function collectAssets(dir: string, baseDir?: string): BundleAssets;
6
+ //# sourceMappingURL=bundle.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ export function collectAssets(dir, baseDir) {
4
+ const base = baseDir ?? dir;
5
+ const assets = {};
6
+ const fileList = [];
7
+ const entries = readdirSync(dir, { withFileTypes: true });
8
+ for (const entry of entries) {
9
+ const fullPath = join(dir, entry.name);
10
+ if (entry.isDirectory()) {
11
+ const sub = collectAssets(fullPath, base);
12
+ Object.assign(assets, sub.assets);
13
+ fileList.push(...sub.fileList);
14
+ }
15
+ else if (entry.isFile()) {
16
+ const relPath = relative(base, fullPath);
17
+ const content = readFileSync(fullPath);
18
+ assets[relPath] = content.toString("base64");
19
+ fileList.push(relPath);
20
+ }
21
+ }
22
+ return { assets, fileList };
23
+ }
24
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1,11 @@
1
+ interface CliConfig {
2
+ token?: string;
3
+ apiUrl?: string;
4
+ }
5
+ export declare function getConfigDir(): string;
6
+ export declare function readCliConfig(): CliConfig;
7
+ export declare function writeCliConfig(config: CliConfig): void;
8
+ export declare function getToken(): string | undefined;
9
+ export declare function getApiUrl(): string;
10
+ export {};
11
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const CONFIG_DIR = join(homedir(), ".creek");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
+ export function getConfigDir() {
7
+ return CONFIG_DIR;
8
+ }
9
+ export function readCliConfig() {
10
+ if (!existsSync(CONFIG_FILE))
11
+ return {};
12
+ try {
13
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ export function writeCliConfig(config) {
20
+ if (!existsSync(CONFIG_DIR)) {
21
+ mkdirSync(CONFIG_DIR, { recursive: true });
22
+ }
23
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
24
+ }
25
+ export function getToken() {
26
+ return process.env.CREEK_TOKEN ?? readCliConfig().token;
27
+ }
28
+ export function getApiUrl() {
29
+ return (process.env.CREEK_API_URL ??
30
+ readCliConfig().apiUrl ??
31
+ "https://api.creek.dev");
32
+ }
33
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Bundle an SSR server entry point into a single standalone worker script.
3
+ * Uses esbuild (same bundler as wrangler) with node: builtins as externals.
4
+ */
5
+ export declare function bundleSSRServer(entryPoint: string): Promise<string>;
6
+ //# sourceMappingURL=ssr-bundle.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { build } from "esbuild";
2
+ /**
3
+ * Bundle an SSR server entry point into a single standalone worker script.
4
+ * Uses esbuild (same bundler as wrangler) with node: builtins as externals.
5
+ */
6
+ export async function bundleSSRServer(entryPoint) {
7
+ const result = await build({
8
+ entryPoints: [entryPoint],
9
+ bundle: true,
10
+ format: "esm",
11
+ platform: "neutral",
12
+ target: "es2022",
13
+ write: false,
14
+ minify: false,
15
+ external: [
16
+ "node:async_hooks",
17
+ "node:stream",
18
+ "node:stream/web",
19
+ "node:buffer",
20
+ "node:util",
21
+ "node:events",
22
+ "node:crypto",
23
+ "node:path",
24
+ "node:url",
25
+ "node:string_decoder",
26
+ "node:diagnostics_channel",
27
+ "node:process",
28
+ "node:fs",
29
+ "node:os",
30
+ "node:child_process",
31
+ "node:http",
32
+ "node:https",
33
+ "node:net",
34
+ "node:tls",
35
+ "node:zlib",
36
+ "node:perf_hooks",
37
+ "node:worker_threads",
38
+ ],
39
+ conditions: ["workerd", "worker", "import"],
40
+ mainFields: ["module", "main"],
41
+ logLevel: "warning",
42
+ });
43
+ if (result.errors.length > 0) {
44
+ throw new Error(`esbuild: ${result.errors.map((e) => e.text).join(", ")}`);
45
+ }
46
+ return result.outputFiles[0].text;
47
+ }
48
+ //# sourceMappingURL=ssr-bundle.js.map
package/package.json CHANGED
@@ -1,24 +1,46 @@
1
1
  {
2
2
  "name": "creek",
3
- "description": "Configurable stream aggregator",
4
- "version": "0.2.2",
5
- "author": {
6
- "name": "Andy Kent",
7
- "email": "andy@forward.co.uk"
3
+ "version": "0.3.0-alpha.2",
4
+ "description": "CLI for the Creek deployment platform",
5
+ "type": "module",
6
+ "bin": {
7
+ "creek": "./dist/index.js"
8
8
  },
9
- "directories": {
10
- "lib" : "./lib"
11
- },
12
- "main": "lib/bootstrap",
13
- "bin": "bin/creek",
14
- "dependencies": {
15
- "coffee-script": "1.0.1"
16
- },
17
- "engines": [
18
- "node"
9
+ "files": [
10
+ "dist",
11
+ "!dist/**/*.test.*",
12
+ "!dist/**/*.map"
13
+ ],
14
+ "keywords": [
15
+ "creek",
16
+ "cli",
17
+ "deployment",
18
+ "cloudflare",
19
+ "workers"
19
20
  ],
21
+ "author": "SolCreek",
22
+ "license": "Apache-2.0",
23
+ "homepage": "https://creek.dev",
20
24
  "repository": {
21
25
  "type": "git",
22
- "url": "http://github.com/andykent/creek.git"
26
+ "url": "https://github.com/solcreek/creek.git",
27
+ "directory": "packages/cli"
28
+ },
29
+ "dependencies": {
30
+ "citty": "^0.1.6",
31
+ "consola": "^3.4.2",
32
+ "smol-toml": "^1.3.1",
33
+ "esbuild": "^0.25.0",
34
+ "@solcreek/sdk": "0.1.0-alpha.1"
35
+ },
36
+ "devDependencies": {
37
+ "typescript": "^5.8.2",
38
+ "@types/node": "^22.13.10"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "dev": "tsc --watch",
43
+ "typecheck": "tsc --noEmit",
44
+ "clean": "rm -rf dist"
23
45
  }
24
46
  }
package/README.md DELETED
@@ -1,105 +0,0 @@
1
- When you've got a hosepipe, what you need is a Creek!
2
- =====================================================
3
- Creek is a dead simple way to run performant summary analysis over time series data. Creek is written in coffee-script and has only been tested with node 0.2.5.
4
-
5
- Current Status
6
- --------------
7
- It's early days for the project and there may be some warts however Creek is currently used in production at Forward to analyse over 35 million messages per day.
8
-
9
- The command line tool and configuration interface is fairly stable however the code level interfaces not so much.
10
-
11
- Hello World Example
12
- -------------------
13
-
14
- Get the code with either `npm install creek` or clone this repo and either add creek to your path or use ./bin/creek in place.
15
-
16
- then create a file somewhere called `hello.creek` containing the following...
17
-
18
- parser 'words'
19
- interface 'rest'
20
- track 'unique-words', aggregator: distinct.alltime
21
- track 'count', aggregator: count.alltime
22
-
23
- Then run `echo 'hello world from creek' | creek hello.creek`
24
-
25
- To see Creek in action run `curl "http://localhost:8080/"` from another window.
26
-
27
- Twitter Example
28
- ---------------
29
-
30
- As with the hello world example above but using this config instead...
31
-
32
- parser 'json', seperatedBy: '\r'
33
- interface 'rest'
34
-
35
- track 'languages-seen'
36
- aggregator: distinct.alltime
37
- field: (o) -> if o.user then o.user.lang else undefined
38
-
39
- track 'popular-words'
40
- aggregator: popular.timeboxed
41
- field: (o) -> if o.text then o.text.toLowerCase().split(' ') else undefined
42
- period: 60
43
- precision: 5
44
- top: 10
45
- before: (v) -> if v and v.length > 4 then v else undefined
46
-
47
- track 'popular-urls'
48
- aggregator: popular.timeboxed
49
- field: (o) -> if o.text then o.text.split(' ') else undefined
50
- period: 60*30
51
- precision: 60
52
- top: 5
53
- before: (v) -> if v and v.indexOf('http://') is 0 then v else undefined
54
-
55
- Then run `curl http://stream.twitter.com/1/statuses/sample.json -u USERNAME:PASSWORD | creek twitter.creek` and visit `http://localhost:8080/`
56
-
57
- Parsers
58
- -------
59
- You must choose a parser, the currently available options are...
60
-
61
- * words - this pushes each word to the aggregator using the current timestamp
62
- * json - expects line separated JSON objects
63
- * chunked - this is a generic parser which can chunk a stream based on any string or regex
64
- * zeromq - allows subscription to a zeromq channel rather than input on stdin.
65
-
66
- Currently all the parsers expect utf8 or a subset thereof.
67
-
68
- Interfaces
69
- ----------
70
- There is currently only one interface available...
71
-
72
- * rest - this makes a JSON rest api available running on localhost:8080 by default.
73
-
74
- Aggregators
75
- -----------
76
- The currently available aggregators are...
77
-
78
- * count.alltime
79
- * count.timeboxed
80
- * distinct.alltime
81
- * distinct.timeboxed
82
- * max.alltime
83
- * max.timeboxed
84
- * mean.alltime
85
- * mean.timeboxed
86
- * min.alltime
87
- * min.timeboxed
88
- * popular.timeboxed
89
- * recent.limited
90
- * sum.alltime
91
- * sum.timeboxed
92
-
93
- All aggregators support `field` and `before` options and timeboxed ones also support `period` and `precision` settings.
94
-
95
- * field - defaults to the whole object, can be a string in which case it uses that key on the object, or an integer in which case it uses that as a constant value, or a function which takes an object and returns a value to be used.
96
- * before - a function that takes each chunks field right before it is about to be pushed to the aggregator, you should return the original value, a modified value or undefined if you would like this chunk to be skipped.
97
- * period - this is the period in seconds over which you would like the timeboxed aggregation to run. A value of 60 will keep track of the value over the last rolling 1 min window. The default value is 60 seconds.
98
- * precision - the accuracy of the rolling time window specified in seconds. I general lower the value used here the more memory will be required. The default value is 1 second.
99
-
100
- Notes
101
- -----
102
- Please note that the current aggregator implementations are fairly immature and they may not be optimal in terms of RAM or CPU usage at this point.
103
- What I can say though is they are efficient enough for most use cases and Creek can comfortably handle dozens of aggregators running across 1,000s of records per second.
104
-
105
- If anyone would like to contribute interfaces, parsers or aggregators they would be gladly received.
package/bin/creek DELETED
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- require('coffee-script')
4
- process.title = 'creek'
5
- configFile = process.argv[2]
6
- if(configFile===undefined) {
7
- console.log('Usage: `creek myconf.creek`')
8
- process.exit(1)
9
- }
10
- require('./../lib/runner').boot(configFile)
@@ -1,6 +0,0 @@
1
- parser 'words'
2
- interface 'rest'
3
-
4
- track 'unique-words', aggregator: distinct.alltime
5
-
6
- track 'count', aggregator: count.alltime
@@ -1,23 +0,0 @@
1
- parser 'json', seperatedBy: '\r'
2
- interface 'rest'
3
-
4
- track 'languages-seen',
5
- aggregator: distinct.alltime
6
- field: (o) -> if o.user then o.user.lang else undefined
7
-
8
- track 'popular-words',
9
- aggregator: popular.timeboxed
10
- field: (o) -> if o.text then o.text.toLowerCase().split(' ') else undefined
11
- period: 60
12
- precision: 5
13
- top: 10
14
- keep: 5
15
- before: (v) -> if v and v.length > 4 then v else undefined
16
-
17
- track 'popular-urls',
18
- aggregator: popular.timeboxed
19
- field: (o) -> if o.text then o.text.split(' ') else undefined
20
- period: 60*30
21
- precision: 60
22
- top: 5
23
- before: (v) -> if v and v.indexOf('http://') is 0 then v else undefined
@@ -1,16 +0,0 @@
1
- parser 'zeromq',
2
- url: 'tcp://example:5555',
3
- channel: 'redirect',
4
- as: 'json',
5
- bucketedBy: 'site',
6
- timestampedBy:'timestamp'
7
-
8
- interface 'rest'
9
-
10
- track 'count', aggregator: count.alltime
11
- track 'popular-keywords',
12
- aggregator: popular.timeboxed
13
- field: (o) -> unescape(o.keyword)
14
- period: 60
15
- precision: 5
16
- top: 10
@@ -1,27 +0,0 @@
1
- events = require('events')
2
-
3
- class Aggregator
4
- constructor: (name, implementation, opts) ->
5
- @name = name
6
- @implementation = implementation
7
- @opts = opts
8
- @implementation.init.call(this, @opts)
9
- @cachedValue = null
10
- @events = new events.EventEmitter()
11
- on: (event, callback) ->
12
- @events.on(event, callback)
13
- push: (time, values) ->
14
- values = [values] unless Array.isArray(values)
15
- for value in values
16
- value = @opts.before.call(this, value) if @opts.before
17
- @implementation.push.call(this, time, value) unless value is undefined
18
- oldValue = @cachedValue
19
- @events.emit('change', @cachedValue, oldValue) unless @compute() is oldValue
20
- compute: ->
21
- @cachedValue = @implementation.compute.call(this)
22
- @cachedValue = @opts.after.call(this, @cachedValue) if @opts.after
23
- @cachedValue
24
- value: ->
25
- @cachedValue
26
-
27
- exports.buildAggregator = (name, implementation)-> ((opts) -> new Aggregator(name, implementation, opts))
@@ -1,15 +0,0 @@
1
- aggregator = require('../aggregator')
2
- timeboxedAggregator = require('../timeboxed-aggregator')
3
-
4
- exports.alltime = aggregator.buildAggregator 'Alltime Count',
5
- init: (opts) -> @count = 0
6
- push: (time, value) -> @count++
7
- compute: -> @count
8
-
9
- exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Count',
10
- recalculateBlockData: (blockData, value) ->
11
- blockData + 1
12
- computeFromBlocks: (blocks) ->
13
- count = 0
14
- count += block.data for block in blocks
15
- count
@@ -1,20 +0,0 @@
1
- aggregator = require('../aggregator')
2
- timeboxedAggregator = require('../timeboxed-aggregator')
3
-
4
-
5
- exports.alltime = aggregator.buildAggregator 'Alltime Distinct',
6
- init: (opts) -> @records = []
7
- push: (time, value) -> @records.push(value) if @records.indexOf(value) is -1
8
- compute: -> @records
9
-
10
-
11
- exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Distinct',
12
- recalculateBlockData: (blockData, value) ->
13
- blockData += value
14
- blockData.push(value) if blockData.indexOf(value) is -1
15
- computeFromBlocks: (blocks) ->
16
- uniques = []
17
- for block in blocks
18
- for value in block
19
- uniques.push(value) if uniques.indexOf(value) is -1
20
- uniques
@@ -1,16 +0,0 @@
1
- aggregator = require('../aggregator')
2
- timeboxedAggregator = require('../timeboxed-aggregator')
3
-
4
- exports.alltime = aggregator.buildAggregator 'Alltime Maximum',
5
- init: (opts) -> @max = null
6
- push: (time, value) -> @max = value if @max == null or @max < value
7
- compute: -> @max
8
-
9
- exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Maximum',
10
- recalculateBlockData: (blockData, value) ->
11
- if blockData is null or blockData < value then value else blockData
12
- computeFromBlocks: (blocks) ->
13
- max = null
14
- for block in blocks
15
- max = block.data if max == null or max < block.data
16
- max
@@ -1,27 +0,0 @@
1
- aggregator = require('../aggregator')
2
- timeboxedAggregator = require('../timeboxed-aggregator')
3
-
4
- exports.alltime = aggregator.buildAggregator 'Alltime Mean',
5
- init: (opts) ->
6
- @count = 0
7
- @total = 0
8
- push: (time, value) ->
9
- @count++
10
- @total += value
11
- compute: -> @total / @count
12
-
13
- exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Mean',
14
- recalculateBlockData: (blockData, value) ->
15
- blockData.count++
16
- blockData.total += value
17
- blockData
18
- defaultBlockValue: ->
19
- {total:0, count:0}
20
- computeFromBlocks: (blocks) ->
21
- total = 0
22
- count = 0
23
- for block in blocks
24
- total += block.data.total
25
- count += block.data.count
26
- return 0 if count is 0
27
- total / count
@@ -1,16 +0,0 @@
1
- aggregator = require('../aggregator')
2
- timeboxedAggregator = require('../timeboxed-aggregator')
3
-
4
- exports.alltime = aggregator.buildAggregator 'Alltime Minimum',
5
- init: (opts) -> @max = null
6
- push: (time, value) -> @max = value if @max == null or @max > value
7
- compute: -> @max
8
-
9
- exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Minimum',
10
- recalculateBlockData: (blockData, value) ->
11
- if blockData is null or blockData > value then value else blockData
12
- computeFromBlocks: (blocks) ->
13
- min = null
14
- for block in blocks
15
- min = block.data if min == null or min > block.data
16
- min
@@ -1,27 +0,0 @@
1
- timeboxedAggregator = require('../timeboxed-aggregator')
2
-
3
- exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Popular',
4
- init: (opts) ->
5
- @numberOfResultsToKeep = opts.top
6
- @numberOfResultsToKeep = 10 if @numberOfResultsToKeep is null
7
- defaultBlockValue: () -> {}
8
- closeBlock: (block) ->
9
- values = Object.keys(block.data)
10
- topValues = values.sort((a,b) -> block.data[b] - block.data[a])
11
- topValues = topValues.slice(0, @numberOfResultsToKeep) if numberOfResultsToKeep?
12
- topValues.reduce ((m,v) -> m[v] = block.data[v]; m), {}
13
- recalculateBlockData: (blockData, value) ->
14
- blockData[value] ?= 0
15
- blockData[value]++
16
- blockData
17
- computeFromBlocks: (blocks) ->
18
- completeBlocks = blocks.slice(0,blocks.length-1)
19
- foldBlocksTogether = (m,v) ->
20
- for value, count of v.data
21
- m[value] = 0 unless m[value]?
22
- m[value] += count
23
- m
24
- foldedResults = completeBlocks.reduce foldBlocksTogether, {}
25
- topValues = Object.keys(foldedResults).sort((a,b) -> foldedResults[b] - foldedResults[a])
26
- topValues = topValues.slice(0, @numberOfResultsToKeep) if @numberOfResultsToKeep?
27
- topValues.reduce ((m,v) -> m.push(value:v, count:foldedResults[v]); m), []
@@ -1,10 +0,0 @@
1
- aggregator = require('../aggregator')
2
-
3
- exports.limited = aggregator.buildAggregator 'Most Recent',
4
- init: (opts) ->
5
- @count = opts.count or 10
6
- @items = []
7
- push: (time, value) ->
8
- @items.unshift(value)
9
- @items.pop() if @items.length > @count
10
- compute: -> @items
@@ -1,17 +0,0 @@
1
- aggregator = require('../aggregator')
2
- timeboxedAggregator = require('../timeboxed-aggregator')
3
-
4
-
5
- exports.alltime = aggregator.buildAggregator 'Alltime Sum',
6
- init: (opts) -> @total = 0
7
- push: (time, value) -> @total += value
8
- compute: -> @total
9
-
10
-
11
- exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Sum',
12
- recalculateBlockData: (blockData, value) ->
13
- blockData += value
14
- computeFromBlocks: (blocks) ->
15
- total = 0
16
- total += block.data for block in blocks
17
- total
package/lib/bootstrap.js DELETED
@@ -1,5 +0,0 @@
1
- require('coffee-script')
2
- var creek = require('./creek')
3
- for(var key in creek) {
4
- exports[key] = creek[key]
5
- }
@@ -1,33 +0,0 @@
1
- class CompoundAggregator
2
- constructor: ->
3
- @aggregators = {}
4
- track: (name, opts) ->
5
- getValue = switch typeof opts.field
6
- when 'string' then ((o) -> o[opts.field])
7
- when 'number' then ((o) -> opts.field)
8
- when 'function' then ((o) -> opts.field(o))
9
- else ((o) -> o)
10
- @aggregators[name] = {getValue:getValue, aggregator:opts.aggregator(opts)}
11
- console.log("Tracking '#{@aggregators[name].aggregator.name}' as '#{name}' with #{JSON.stringify(opts)}")
12
- on: (name, event, callback) ->
13
- if arguments.length == 3
14
- @aggregators[name].aggregator.on(event, callback)
15
- else
16
- event = name
17
- callback = event
18
- agg.aggregator.on(event, callback) for name, agg of @aggregators
19
- push: (time, obj) ->
20
- for name, agg of @aggregators
21
- val = agg.getValue(obj)
22
- agg.aggregator.push(time, val)
23
- value: (name) ->
24
- if typeof name is 'string'
25
- console.log("Aggregation '#{name}' does not exist!") unless @aggregators[name]
26
- @aggregators[name].aggregator.value()
27
- else
28
- ret = {}
29
- for name, agg of @aggregators
30
- ret[name] = agg.aggregator.value()
31
- ret
32
-
33
- exports.create = -> new CompoundAggregator()
package/lib/creek.coffee DELETED
@@ -1,13 +0,0 @@
1
- exports.aggregator = require('./aggregator')
2
- exports.compoundAggregator = require('./compound-aggregator')
3
- exports.lazyBucketedAggregator = require('./lazy-bucketed-aggregator')
4
-
5
- exports.aggregators =
6
- count: require('./aggregators/count')
7
- distinct: require('./aggregators/distinct')
8
- max: require('./aggregators/max')
9
- mean: require('./aggregators/mean')
10
- min: require('./aggregators/min')
11
- popular: require('./aggregators/popular')
12
- recent: require('./aggregators/recent')
13
- sum: require('./aggregators/sum')
@@ -1,30 +0,0 @@
1
- http = require('http')
2
- url = require('url')
3
-
4
- exports.init = (agg, opts) ->
5
- console.log("Rest Server Started")
6
- server = http.createServer (req, res) ->
7
- path = url.parse(req.url).pathname
8
- path = path.replace(opts.path, '') if opts.path
9
- pathParts = path.split('/')
10
- pathParts.shift()
11
- buildResponse = (obj) ->
12
- callback = url.parse(req.url, true).query?.callback
13
- if callback?
14
- "#{callback}(#{JSON.stringify(obj)});"
15
- else
16
- JSON.stringify(obj)
17
- try
18
- result = if pathParts.length is 0
19
- buildResponse(agg.value())
20
- else if pathParts.length is 1
21
- buildResponse(agg.value(null, pathParts[0]))
22
- else
23
- buildResponse(agg.value(pathParts[1], pathParts[0]))
24
- catch e
25
- res.writeHead(500, 'Content-Type': 'application/json; charset=utf-8')
26
- res.end(buildResponse(message: "Unable to display requested data"))
27
- return
28
- res.writeHead(200, 'Content-Type': 'application/json')
29
- res.end(result)
30
- server.listen (opts.port or 8080), (opts.host or 'localhost')
@@ -1,15 +0,0 @@
1
- http = require('http')
2
- io = require('socket.io')
3
-
4
- exports.init = (agg, opts) ->
5
- server = http.createServer (req, res) ->
6
- res.writeHead(200, {'Content-Type': 'text/html'})
7
- res.write('<h1>Hello world</h1>')
8
- res.end()
9
-
10
- server.listen(8888)
11
-
12
- socket = io.listen(server)
13
-
14
- agg.on 'change', (newValue) ->
15
- socket.broadcast(JSON.stringify(newValue))
@@ -1,35 +0,0 @@
1
- compoundAggregator = require('./compound-aggregator')
2
-
3
- class LazyBucketedAggregator
4
- constructor: ->
5
- @aggregatorOpts = {}
6
- @aggregators = {}
7
- @events = []
8
- track: (name, opts) ->
9
- @aggregatorOpts[name] = opts
10
- console.log("Lazy Tracking '#{name}' with #{JSON.stringify(opts)}")
11
- on: (name, event, callback) ->
12
- if arguments.length == 2
13
- event = name
14
- callback = event
15
- name = null
16
- @events.push( name:name, event:event, callback:callback )
17
- push: (bucket, time, obj) ->
18
- unless @aggregators[bucket]
19
- @aggregators[bucket] = compoundAggregator.create()
20
- @aggregators[bucket].track(name, opts) for name, opts of @aggregatorOpts
21
- for e in @events
22
- @aggregators[bucket].on e.name, e.event, (newValue, oldValue) -> e.callback(bucket, newValue, oldValue)
23
- @aggregators[bucket].push(time, obj)
24
- value: (name, bucket) ->
25
- if bucket
26
- @aggregators[bucket].value(name)
27
- else
28
- ret = {}
29
- for bucket, agg of @aggregators
30
- ret[bucket] = agg.value(name)
31
- ret
32
- buckets: ->
33
- Object.keys(@aggregators)
34
-
35
- exports.create = -> new LazyBucketedAggregator()
@@ -1,17 +0,0 @@
1
- class ChunkedStream
2
- constructor: (opts, recordHandler) ->
3
- @seperator = opts.seperatedBy or "\n"
4
- @stream = process.openStdin()
5
- @stream.setEncoding('utf8')
6
- @patialData = ""
7
- @recordHandler = recordHandler
8
- @stream.on 'data', @dataHandler
9
- dataHandler: (data) =>
10
- @patialData += data
11
- parts = @patialData.split(@seperator)
12
- @patialData = parts.pop() # last part will be an incomplete chunk or an empty string if we are lucky
13
- for chunk in parts
14
- @recordHandler(chunk) if chunk
15
-
16
-
17
- exports.init = (agg, opts, handler) -> new ChunkedStream(opts, handler)
@@ -1,9 +0,0 @@
1
- chunkedStream = require('./chunked')
2
-
3
- exports.init = (agg, opts, handler) ->
4
- chunkedStream.init agg, opts, (chunk) ->
5
- try
6
- parsedLine = JSON.parse(chunk)
7
- catch e
8
- handler(parsedLine) if parsedLine
9
-
@@ -1,6 +0,0 @@
1
- chunkedStream = require('./chunked')
2
-
3
- exports.init = (agg, opts, handler) ->
4
- opts.seperatedBy = /\W/
5
- chunkedStream.init agg, opts, (chunk) ->
6
- handler(chunk)
@@ -1,16 +0,0 @@
1
- zeromq = require('zeromq')
2
-
3
-
4
- exports.init = (agg, opts, handler) ->
5
- socket = zeromq.createSocket('sub')
6
- socket.connect(opts.url)
7
- socket.subscribe(opts.channel) if opts.channel
8
- socket.on 'message', (ch, data) ->
9
- utf8Data = data.toString('utf8')
10
- if opts.as is 'json'
11
- try
12
- handler(JSON.parse(utf8Data))
13
- catch e
14
- console.log("ERR: failed to JSON parse - #{utf8Data}")
15
- else
16
- handler(utf8Data)
package/lib/runner.coffee DELETED
@@ -1,39 +0,0 @@
1
- creek = require('./creek')
2
-
3
- agg = creek.lazyBucketedAggregator.create()
4
-
5
- publicInterface = {}
6
-
7
- publicInterface.track = (name, opts) -> agg.track(name, opts)
8
-
9
- for name, code of creek.aggregators
10
- publicInterface[name] = code
11
-
12
- publicInterface.interface = (interfaceName, opts) ->
13
- opts ?= {}
14
- require("./interfaces/#{interfaceName}").init(agg, opts)
15
-
16
- publicInterface.parser = (parserName, opts) ->
17
- opts ?= {}
18
- require("./parsers/#{parserName}").init agg, opts, (record) ->
19
- bucket = if opts.bucketedBy?
20
- if typeof opts.bucketedBy is 'string'
21
- record[opts.bucketedBy]
22
- else
23
- opts.bucketedBy(record)
24
- else
25
- null
26
- timestamp = if opts.timestampedBy?
27
- if typeof opts.timestampedBy is 'string'
28
- new Date(record[opts.timestampedBy])
29
- else
30
- opts.timestampedBy(record)
31
- else
32
- new Date()
33
- agg.push((bucket or 'default'), timestamp, record)
34
-
35
- exports.boot = (configFile) ->
36
- Script = process.binding('evals').Script
37
- code = require('fs').readFileSync(configFile, 'utf8')
38
- jsCode = require('coffee-script').compile(code)
39
- Script.runInNewContext(jsCode, publicInterface, configFile)
@@ -1,64 +0,0 @@
1
- events = require('events')
2
-
3
- class TimeboxedAggregator
4
- constructor: (name, implementation, opts) ->
5
- @name = name
6
- @opts = opts
7
- @implementation = implementation
8
- @period = (opts.period or 60) * 1000
9
- @precision = (opts.precision or 1) * 1000
10
- @historySize = opts.keep or 1
11
- @history = []
12
- @cachedValue = null
13
- @blocks = []
14
- @implementation.init.call(this, opts) if @implementation.init
15
- @events = new events.EventEmitter()
16
- @staleCache = false
17
- setInterval (=> @compute(true)), @precision
18
- on: (event, callback) ->
19
- @events.on(event, callback)
20
- push: (time, values) ->
21
- currentBlock = @maybeCreateNewBlock(time)
22
- values = [values] unless Array.isArray(values)
23
- for value in values
24
- value = @opts.before.call(this, value) if @opts.before
25
- currentBlock.data = @implementation.recalculateBlockData.call(this, currentBlock.data, value, currentBlock.time, time) unless value is undefined
26
- oldValue = @cachedValue
27
- @compute()
28
- @events.emit('change', @cachedValue, oldValue) if @cachedValue != oldValue
29
- compute: (force=false) ->
30
- @cleanup()
31
- if force or @staleCache
32
- @cachedValue = @implementation.computeFromBlocks.call(this, @blocks)
33
- @cachedValue = @opts.after.call(this, @cachedValue) if @opts.after
34
- @updateHistory()
35
- @staleCache = false
36
- @cachedValue
37
- value: ->
38
- @cachedValue
39
- maybeCreateNewBlock: (time) ->
40
- if @blocks.length == 0
41
- @blocks.push( time:time, data:@defaultBlockValue() )
42
- return @blocks[@blocks.length-1]
43
- lastBlock = @blocks[@blocks.length-1]
44
- diff = time - lastBlock.time.getTime()
45
- if diff > @precision
46
- @blocks.push( time:time, data:@defaultBlockValue() )
47
- @blocks[@blocks.length-2].data = @implementation.closeBlock(lastBlock) if @implementation.closeBlock
48
- @staleCache = true
49
- @blocks[@blocks.length-1]
50
- cleanup: ->
51
- periodThreshold = new Date().getTime() - @period
52
- loop
53
- break if @blocks.length == 0 or @blocks[0].time.getTime() > periodThreshold
54
- @blocks.shift()
55
- defaultBlockValue: ->
56
- if @implementation.defaultBlockValue then @implementation.defaultBlockValue.call(this) else null
57
- updateHistory: ->
58
- @history.unshift(@cachedValue)
59
- @history.pop() if @history.length > @historySize
60
- @history
61
-
62
- exports.TimeboxedAggregator = TimeboxedAggregator
63
-
64
- exports.buildTimeboxedAggregator = (name, implementation)-> ((opts) -> new TimeboxedAggregator(name, implementation, opts))